mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-25 08:48:44 +00:00
Compare commits
215 Commits
snapshot-f
...
version-0.
Author | SHA1 | Date | |
---|---|---|---|
31edb6a7fe | |||
338670118d | |||
32573bdf78 | |||
bd683253a5 | |||
75279c2e6c | |||
9e33ac564d | |||
7fe7c2d0a0 | |||
79b9f0e921 | |||
a75ec9a0c5 | |||
b11f1fb394 | |||
3c927693f1 | |||
cb41f239b8 | |||
8648c67a98 | |||
d11ae90ad6 | |||
8395dfaa52 | |||
3273e56b17 | |||
9b1786dc01 | |||
429513f337 | |||
6ee8dad45f | |||
f380d44dd2 | |||
2cf14e543c | |||
1669741d19 | |||
2be41d8714 | |||
3e7e416a2a | |||
b377af846a | |||
6b5b971368 | |||
f6e09f1d57 | |||
f069358764 | |||
c509b6da38 | |||
e788929d67 | |||
18c94943cb | |||
b24a990043 | |||
a4295df20d | |||
3398b812ce | |||
9e9f266878 | |||
7cbd6705e1 | |||
1eb078d916 | |||
82201286d4 | |||
dc84a9ef55 | |||
b2cf1cc83b | |||
848353d804 | |||
e8978a7f92 | |||
9d24e61949 | |||
c748977a1b | |||
fcae58168e | |||
564a5e1095 | |||
3d7df30db4 | |||
a63d5d0c65 | |||
e923ce1013 | |||
231ca165a1 | |||
35c1150f80 | |||
37f89110e7 | |||
4766d2440d | |||
ad07f3e1f8 | |||
7ab2f332e9 | |||
00a3f6632c | |||
9afbe7c3a9 | |||
27682870a1 | |||
98031b53cb | |||
e1fd6d51c4 | |||
0fd2430866 | |||
9e9d1960c8 | |||
64380dd849 | |||
c21e9131a0 | |||
ec04986434 | |||
f7d217a126 | |||
f9cca885ed | |||
64f706a98e | |||
7ff7ae3a0c | |||
d7e295881f | |||
9fd55746ca | |||
121f9aa477 | |||
c8edd14833 | |||
f7b6028ba1 | |||
bf2836b69f | |||
d3f3e25ed5 | |||
d0839dc93c | |||
5540f85466 | |||
1a66f45425 | |||
90ac1bb9c3 | |||
faafacc722 | |||
4508dea767 | |||
7c6c02ab28 | |||
e1a63752ca | |||
059b746597 | |||
11e1fb86b2 | |||
eb8bf12047 | |||
66b2b4ff4c | |||
a6e0741175 | |||
2b7432167d | |||
0a12c76829 | |||
3fe6828550 | |||
27a17183ae | |||
94bf10fe6e | |||
610915bf94 | |||
3e26ea94ee | |||
e6190b4fe2 | |||
1c582621aa | |||
b91da93858 | |||
522dafca69 | |||
82f0f177fb | |||
9c79ce3272 | |||
85eae36c28 | |||
485bc618e0 | |||
6f6f9af17d | |||
65fe6a002c | |||
ede497ee85 | |||
5a56b5d9b4 | |||
e10a1e40da | |||
8816bc89c3 | |||
759972b70c | |||
425629de52 | |||
9dec3c517b | |||
be69836b5b | |||
d528f984d4 | |||
3335e29535 | |||
04547577f7 | |||
2d9ea8b73f | |||
1292c04040 | |||
7a1b99bab3 | |||
e7735619c1 | |||
a06db0b7a8 | |||
baac007c09 | |||
889697f86f | |||
f009d50631 | |||
c9e9d62098 | |||
196488ad6e | |||
81a5d868b6 | |||
0d3f886e5a | |||
f5f02a2234 | |||
00b55c583e | |||
35c079e1dd | |||
df262e52e0 | |||
0e7c55e108 | |||
b8ac6d5d61 | |||
5f5a9db292 | |||
c70f73d9cd | |||
0651fa22af | |||
9d669de989 | |||
0cc6d7e618 | |||
60e01a303a | |||
ae4248b5aa | |||
4e0ccaa147 | |||
e0bb0ef2de | |||
8ca59e0a5b | |||
e022284ca7 | |||
e853c67b67 | |||
b3fb15e550 | |||
c1f0615964 | |||
4e181d9932 | |||
77689dd75e | |||
478f539bee | |||
a607f35805 | |||
7ba4f142a6 | |||
60ce1d8c09 | |||
0b8549def3 | |||
1f5129facc | |||
fff1c2a131 | |||
42ad9aec41 | |||
dd3fa7fa69 | |||
5c0b1ea2c3 | |||
d417c2c93a | |||
1d42b3eb16 | |||
62d4c17aba | |||
76028ddcaa | |||
8829408e54 | |||
0a35d02404 | |||
8c882b8476 | |||
3d729d67e6 | |||
984e37bcd8 | |||
3f16f4c0c3 | |||
29b8025c41 | |||
9f4cb02764 | |||
a32c4f4936 | |||
20a13161c5 | |||
c37056f942 | |||
2b6d1ea468 | |||
7b2e9c4f55 | |||
13c64de280 | |||
2a6008f4dc | |||
5623c6f6c2 | |||
7ecc005232 | |||
bab994edbe | |||
9ebf07665a | |||
bd947c9e16 | |||
ea28c007c1 | |||
389e7a1515 | |||
b5ceb846e2 | |||
e9cfb67101 | |||
c05a1d5812 | |||
a3ed016606 | |||
4f0ded6bd6 | |||
925afc95e3 | |||
0e8e93bff9 | |||
bd8246ca5a | |||
c72e6ad23d | |||
52c1018d5e | |||
6911be1d52 | |||
7e27c7a89a | |||
3765f6351c | |||
9281d07e96 | |||
b692f1a60d | |||
68687a5d3b | |||
97b17085be | |||
8a74609c51 | |||
97132665a8 | |||
e9bcec4c07 | |||
570d1a80b5 | |||
5e4305d1d5 | |||
a591f57591 | |||
57bb39319f | |||
974b90f0b7 | |||
71ecde74df | |||
7e34b97998 | |||
1062f00ed4 |
10
.github/actions/install_asciidoctor/action.yml
vendored
Normal file
10
.github/actions/install_asciidoctor/action.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: install_asciidoctor
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- name: Install asciidoctor
|
||||
run: |-
|
||||
sudo apt-get update
|
||||
sudo apt install -y asciidoctor
|
||||
shell: bash
|
16
.github/actions/install_java17/action.yml
vendored
Normal file
16
.github/actions/install_java17/action.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: install_java17
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install Java 17
|
||||
run: |-
|
||||
sudo apt-get update
|
||||
sudo apt install -y openjdk-17-jdk
|
||||
sudo rm /etc/alternatives/java
|
||||
sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java
|
||||
shell: bash
|
||||
- name: Install html2text
|
||||
run: |-
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y html2text
|
||||
shell: bash
|
22
.github/actions/mvn_jar_deploy/action.yml
vendored
Normal file
22
.github/actions/mvn_jar_deploy/action.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: mvn_jar_deploy
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- name: Adjust pom version
|
||||
run: ".circleci/adjust-pom-version.sh"
|
||||
shell: bash
|
||||
- name: restore_cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
||||
path: UPDATE_ME
|
||||
restore-keys: v1-dependencies-{{ checksum "pom.xml" }}
|
||||
- name: Run Maven Jar Deploy
|
||||
run: mvn -s .circleci/mvn-settings.xml -T4 flatten:flatten jar:jar deploy:deploy
|
||||
shell: bash
|
||||
- name: save_cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
path: "~/.m2"
|
||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
61
.github/actions/mvn_verify/action.yml
vendored
Normal file
61
.github/actions/mvn_verify/action.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
name: mvn_verify
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- name: restore_cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
||||
path: UPDATE_ME
|
||||
restore-keys: v1-dependencies-{{ checksum "pom.xml" }}
|
||||
- name: Write .env
|
||||
run: echo "RDBMS_PASSWORD=$RDBMS_PASSWORD" >> qqq-sample-project/.env
|
||||
shell: bash
|
||||
- name: Run Maven Verify
|
||||
run: mvn -s .circleci/mvn-settings.xml -T4 verify
|
||||
shell: bash
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-backend-core
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-backend-module-filesystem
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-backend-module-rdbms
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-backend-module-api
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-middleware-api
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-middleware-javalin
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-middleware-picocli
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-middleware-slack
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-language-support-javascript
|
||||
- uses: "./.github/actions/store_jacoco_site"
|
||||
with:
|
||||
module: qqq-sample-project
|
||||
- name: Save test results
|
||||
run: |-
|
||||
mkdir -p ~/test-results/junit/
|
||||
find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/test-results/junit/ \;
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: actions/upload-artifact@v4.1.0
|
||||
with:
|
||||
path: "~/test-results"
|
||||
- name: save_cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
path: "~/.m2"
|
||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
9
.github/actions/run_asciidoctor/action.yml
vendored
Normal file
9
.github/actions/run_asciidoctor/action.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
name: run_asciidoctor
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Run asciidoctor
|
||||
run: |-
|
||||
cd docs
|
||||
asciidoctor -a docinfo=shared index.adoc
|
||||
shell: bash
|
13
.github/actions/store_jacoco_site/action.yml
vendored
Normal file
13
.github/actions/store_jacoco_site/action.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: store_jacoco_site
|
||||
inputs:
|
||||
module:
|
||||
required: false
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/upload-artifact@v4.1.0
|
||||
with:
|
||||
path: "${{ inputs.module }}/target/site/jacoco/index.html"
|
||||
- uses: actions/upload-artifact@v4.1.0
|
||||
with:
|
||||
path: "${{ inputs.module }}/target/site/jacoco/jacoco-resources"
|
9
.github/actions/upload_docs_site/action.yml
vendored
Normal file
9
.github/actions/upload_docs_site/action.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
name: upload_docs_site
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: scp html to justinsgotskinnylegs.com
|
||||
run: |-
|
||||
cd docs
|
||||
scp index.html dkelkhoff@45.79.44.221:/mnt/first-volume/dkelkhoff/nginx/html/justinsgotskinnylegs.com/qqq-docs.html
|
||||
shell: bash
|
61
.github/workflows/codacy.yml
vendored
Normal file
61
.github/workflows/codacy.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# This workflow checks out code, performs a Codacy security scan
|
||||
# and integrates the results with the
|
||||
# GitHub Advanced Security code scanning feature. For more information on
|
||||
# the Codacy security scan action usage and parameters, see
|
||||
# https://github.com/codacy/codacy-analysis-cli-action.
|
||||
# For more information on Codacy Analysis CLI in general, see
|
||||
# https://github.com/codacy/codacy-analysis-cli.
|
||||
|
||||
name: Codacy Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "security" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "security" ]
|
||||
schedule:
|
||||
- cron: '26 5 * * 4'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codacy-security-scan:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
name: Codacy Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout the repository to the GitHub Actions runner
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||
- name: Run Codacy Analysis CLI
|
||||
uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b
|
||||
with:
|
||||
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
|
||||
# You can also omit the token and run the tools that support default configurations
|
||||
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
verbose: true
|
||||
output: results.sarif
|
||||
format: sarif
|
||||
# Adjust severity of non-security issues
|
||||
gh-code-scanning-compat: true
|
||||
# Force 0 exit code to allow SARIF file generation
|
||||
# This will handover control about PR rejection to the GitHub side
|
||||
max-allowed-issues: 2147483647
|
||||
|
||||
# Upload the SARIF file generated in the previous step
|
||||
- name: Upload SARIF results file
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
93
.github/workflows/codeql.yml
vendored
Normal file
93
.github/workflows/codeql.yml
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "security" ]
|
||||
pull_request:
|
||||
branches: [ "security" ]
|
||||
schedule:
|
||||
- cron: '31 10 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: java-kotlin
|
||||
build-mode: none # This mode only analyzes Java. Set this to 'autobuild' or 'manual' to analyze Kotlin too.
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
20
.github/workflows/deploy.yml
vendored
Normal file
20
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: Kingsrook/qqq/deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
jobs:
|
||||
mvn_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/install_java17"
|
||||
- uses: "./.github/actions/mvn_verify"
|
||||
- uses: "./.github/actions/mvn_jar_deploy"
|
||||
publish_asciidoc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/install_asciidoctor"
|
||||
- uses: "./.github/actions/run_asciidoctor"
|
||||
- uses: "./.github/actions/upload_docs_site"
|
12
.github/workflows/test_only.yml
vendored
Normal file
12
.github/workflows/test_only.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
name: Kingsrook/qqq/test_only
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
jobs:
|
||||
mvn_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/install_java17"
|
||||
- uses: "./.github/actions/mvn_verify"
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -35,3 +35,5 @@ hs_err_pid*
|
||||
*.swp
|
||||
.flattened-pom.xml
|
||||
dependency-reduced-pom.xml
|
||||
/.env.local
|
||||
/.cache/
|
||||
|
@ -19,7 +19,7 @@ You can also use fine-grained jars:
|
||||
|
||||
## License
|
||||
QQQ - Low-code Application Framework for Engineers. \
|
||||
Copyright (C) 2022. Kingsrook, LLC \
|
||||
Copyright (C) 2020-2024. Kingsrook, LLC \
|
||||
651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States \
|
||||
contact@kingsrook.com | https://github.com/Kingsrook/
|
||||
|
||||
|
21
SECURITY.md
Normal file
21
SECURITY.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
@ -117,3 +117,19 @@ new QTableMetaData().withName("flights").withFields(List.of(
|
||||
.withBehavior(new DateTimeDisplayValueBehavior()
|
||||
.withDefaultZoneId("UTC"))
|
||||
----
|
||||
|
||||
===== CaseChangeBehavior
|
||||
A field can be made to always go through a toUpperCase or toLowerCase transformation, both before it is stored in a backend,
|
||||
and after it is read from a backend, by adding a CaseChangeBehavior to it:
|
||||
|
||||
[source,java]
|
||||
.Examples of using CaseChangeBehavior
|
||||
----
|
||||
new QTableMetaData().withName("item").withFields(List.of(
|
||||
|
||||
new QFieldMetaData("sku", QFieldType.STRING)
|
||||
.withBehavior(CaseChangeBehavior.TO_UPPER_CASE)),
|
||||
|
||||
new QFieldMetaData("username", QFieldType.STRING)
|
||||
.withBehavior(CaseChangeBehavior.TO_LOWER_CASE)),
|
||||
----
|
||||
|
@ -2,16 +2,57 @@
|
||||
== Joins
|
||||
include::../variables.adoc[]
|
||||
|
||||
#TODO#
|
||||
A `QJoinMetaData` is a meta-data object that tells QQQ, essentially “it is possible for these 2 tables to join, here’s how to do it”.
|
||||
|
||||
Joins can be used then, in an application, in a number of possible ways:
|
||||
|
||||
* In a {link-table}, we can specify joins to be “exposed”, e.g., made available to users on a query screen
|
||||
* Also in a Table, as part of an “Association”, which sets up one table as a “parent” of another,
|
||||
such that you can store (and fetch) the child-records at the same time as the parent
|
||||
** A common use-case here may be an order & lineItem table -
|
||||
such that QQQ can generate an API uses to allow you to post an order and its lines in a single request, and they get stored all together
|
||||
* In defining the security field (record lock) on a table,
|
||||
sometimes, it isn’t a field directly on the table, but instead comes from a joined table (possibly even more than just 1 table away).
|
||||
** For example, maybe a lineItem table, doesn't have a clientId, but needs secured by that field
|
||||
- so its recordLock can specify a “joinNameChain” that describes how to get from lineItem to order.clientId
|
||||
* The `QueryAction` can take (through its QueryInput object) zero or more QueryJoin objects,
|
||||
which must make a reference (implicitly or explicitly) to a QJoinMetaData.
|
||||
See the section on <<QueryJoin,QueryJoins>> for more details.
|
||||
|
||||
=== QJoinMetaData
|
||||
Joins are defined in a QQQ Instance in a `*QJoinMetaData*` object.
|
||||
|
||||
#TODO#
|
||||
In this object, we have the concept of a "leftTable" and a "rightTable".
|
||||
There isn't generally anything special about which table is on the "left" and which is on the "right".
|
||||
But the remaining pieces of the QJoinMetaData do all need to line-up with these sides.
|
||||
|
||||
For example:
|
||||
|
||||
* The Type (one-to-one, one-to-many, many-to-one) - where the leftTable comes first, and rightTable comes second
|
||||
(e.g., a one-to-many means 1-row in leftTable has many-rows in rightTable associated with it)
|
||||
* In a JoinOn object, the 1st field name given is from the leftTable;
|
||||
the second fieldName from the rightTable.
|
||||
|
||||
*QJoinMetaData Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the join within the QQQ Instance. #todo infererences or conventions?#
|
||||
|
||||
#TODO#
|
||||
* `name` - *String, Required* - Unique name for the join within the QQQ Instance.
|
||||
** One convention is to name joins based on (leftTable + "Join" + rightTable).
|
||||
** If you do not wish to define join names yourself, the method `withInferredName()`
|
||||
can be called (which defers to
|
||||
`public static String makeInferredJoinName(String leftTable, String rightTable)`),
|
||||
to create a name for the join following the (leftTable + "Join" + rightTable) convention.
|
||||
* `leftTable` - *String, Required* - Name of a {link-table} in the {link-instance}.
|
||||
* `rightTable` - *String, Required* - Name of a {link-table} in the {link-instance}.
|
||||
* `type` - *enum, Required* - cardinality between the two tables in the join.
|
||||
** e.g., `ONE_TO_ONE`, `ONE_TO_MANY` (indicating 1 record in the left table may join
|
||||
to many records in the right table), or `MANY_TO_ONE` (vice-versa).
|
||||
** Note that there is no MANY_TO_MANY option, as a many-to-many is built as multiple QJoinMetaData's
|
||||
going through the intermediary (intersection) table.
|
||||
* `joinOns` - *List<JoinOn>, Required* - fields used to join the tables.
|
||||
Note: In the 2-arg JoinOn constructor, the leftTable's field comes first.
|
||||
Alternatively, the no-arg constructor can be used along with `.withLeftField().withRightField()`
|
||||
* `orderBys` - *List<QFilterOrderBy>* - Optional list of order-by objects,
|
||||
used in some framework-generated queries using the join.
|
||||
The field names are assumed to come from the rightTable.
|
||||
|
||||
#TODO# what else do we need here?
|
||||
|
@ -16,6 +16,7 @@ Processes are defined in a QQQ Instance in a `*QProcessMetaData*` object.
|
||||
In addition to directly building a `QProcessMetaData` object setting its properties directly, there are a few common process patterns that provide *Builder* objects for ease-of-use.
|
||||
See StreamedETLWithFrontendProcess below for a common example
|
||||
|
||||
[#_QProcessMetaData_Properties]
|
||||
*QProcessMetaData Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the process within the QQQ Instance.
|
||||
@ -30,12 +31,13 @@ See below for details.
|
||||
* `permissionRules` - *QPermissionRules object* - define the permission/access rules for the process.
|
||||
See {link-permissionRules} for details.
|
||||
* `steps` and `stepList` - *Map of String → <<QStepMetaData>>* and *List of QStepMetaData* - Defines the <<QFrontendStepMetaData,screens>> and <<QBackendStepMetaData,backend code>> that makes up the process.
|
||||
** `stepList` is the list of steps in the order that they will by default be executed.
|
||||
** `steps` is a map, including all steps from `stepList`, but which may also include steps which can used by the process if its backend steps make the decision to do so, at run-time.
|
||||
** `stepList` is the list of steps in the order that they will be executed
|
||||
(that is to say - this is the _default_ order of execution - but it can be customized - see <<_custom_process_flow>> for details).
|
||||
** `steps` is a map, including all steps from `stepList`, but which may also include steps which can used by the process if its backend steps make the decision to do so, at run-time (e.g., using <<_custom_process_flow>>).
|
||||
** A process's steps are normally defined in one of two was:
|
||||
*** 1) by a single call to `.withStepList(List<QStepMetaData>)`, which internally adds each step into the `steps` map.
|
||||
*** 2) by multiple calls to `.addStep(QStepMetaData)`, which adds a step to both the `stepList` and `steps` map.
|
||||
** If a process also needs optional steps, they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
|
||||
** If a process also needs optional steps (for a <<_custom_process_flow>>), they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
|
||||
* `schedule` - *<<QScheduleMetaData>>* - set up the process to run automatically on the specified schedule.
|
||||
See below for details.
|
||||
* `minInputRecords` - *Integer* - #not used...#
|
||||
@ -214,3 +216,112 @@ But for some cases, doing page-level transactions can reduce long-transactions a
|
||||
* `withFields(List<QFieldMetaData> fieldList)` - Adds additional input fields to the preview step of the process.
|
||||
* `withBasepullConfiguration(BasepullConfiguration basepullConfiguration)` - Add a <<BasepullConfiguration>> to the process.
|
||||
* `withSchedule(QScheduleMetaData schedule)` - Add a <<QScheduleMetaData>> to the process.
|
||||
|
||||
[#_custom_process_flow]
|
||||
==== Custom Process Flow
|
||||
As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default, a process
|
||||
will execute each of its steps in-order, as defined in the `stepList` property.
|
||||
However, a Backend Step can customize this flow #todo - write more clearly here...
|
||||
|
||||
There are generally 2 method to call (in a `BackendStep`) to do a dynamic flow:
|
||||
|
||||
* `RunBackendStepOutput.setOverrideLastStepName(String stepName)`
|
||||
** QQQ's `RunProcessAction` keeps track of which step it "last" ran, e.g., to tell it which one to run next.
|
||||
However, if a step sets the `OverrideLastStepName` property in its output object,
|
||||
then the step named in that property becomes the effective "last" step,
|
||||
thus determining which step comes next.
|
||||
|
||||
* `RunBackendStepOutput.updateStepList(List<String> stepNameList)`
|
||||
** Calling this method changes the process's runtime definition of steps to be executed.
|
||||
Thus allowing a completely custom flow.
|
||||
It should be noted, that the "last" step name (as tracked by QQQ within `RunProcessAction`)
|
||||
does need to be found in the new `stepNameList` - otherwise, the framework will not know where you were,
|
||||
for figuring out where to go next.
|
||||
|
||||
[source,java]
|
||||
.Example of a defining process that can use a flexible flow:
|
||||
----
|
||||
// for a case like this, it would be recommended to define all step names in constants:
|
||||
public final static String STEP_START = "start";
|
||||
public final static String STEP_A = "a";
|
||||
public final static String STEP_B = "b";
|
||||
public final static String STEP_C = "c";
|
||||
public final static String STEP_1 = "1";
|
||||
public final static String STEP_2 = "2";
|
||||
public final static String STEP_3 = "3";
|
||||
public final static String STEP_END = "end";
|
||||
|
||||
// also, to define the possible flows (lists of steps) in constants as well:
|
||||
public final static List<String> LETTERS_STEP_LIST = List.of(
|
||||
STEP_START, STEP_A, STEP_B, STEP_C, STEP_END);
|
||||
|
||||
public final static List<String> NUMBERS_STEP_LIST = List.of(
|
||||
STEP_START, STEP_1, STEP_2, STEP_3, STEP_END);
|
||||
|
||||
// when we define the process's meta-data, we only give a "skeleton" stepList -
|
||||
// we must at least have our starting step, and we may want at least one frontend step
|
||||
// for the UI to show some placeholder(s):
|
||||
QProcessMetaData process = new QProcessMetaData()
|
||||
.withName(PROCESS_NAME)
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData().withName(STEP_START)
|
||||
.withCode(new QCodeReference(/*...*/)),
|
||||
new QFrontendStepMetaData()
|
||||
.withName(STEP_END)
|
||||
));
|
||||
|
||||
// the additional steps get added via `addOptionalStep`, which only puts them in
|
||||
// the process's stepMap, not its stepList!
|
||||
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_A));
|
||||
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_B)
|
||||
.withCode(new QCodeReference(/*...*/)));
|
||||
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_C));
|
||||
|
||||
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_1)
|
||||
.withCode(new QCodeReference(/*...*/)));
|
||||
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_2));
|
||||
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_3)
|
||||
.withCode(new QCodeReference(/*...*/)));
|
||||
|
||||
----
|
||||
|
||||
[source,java]
|
||||
.Example of a process backend step adjusting the process's runtime flow:
|
||||
----
|
||||
/***************************************************************************
|
||||
** look at the value named "which". if it's "letters", then make the process
|
||||
** go through the stepList consisting of letters; else, update the step list
|
||||
** to be the "numbers" steps.
|
||||
**
|
||||
** Also - if the "skipSomeSteps" value is give as true, then set the
|
||||
** overrideLastStepName to skip again (in the letters case, skip past A, B
|
||||
** and C; in the numbers case, skip past 1 and 2).
|
||||
***************************************************************************/
|
||||
public static class StartStep implements BackendStep
|
||||
{
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
Boolean skipSomeSteps = runBackendStepInput.getValueBoolean("skipSomeSteps");
|
||||
|
||||
if(runBackendStepInput.getValueString("which").equals("letters"))
|
||||
{
|
||||
runBackendStepOutput.updateStepList(LETTERS_STEP_LIST);
|
||||
if(BooleanUtils.isTrue(skipSomeSteps))
|
||||
{
|
||||
runBackendStepOutput.setOverrideLastStepName(STEP_C);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
runBackendStepOutput.updateStepList(NUMBERS_STEP_LIST);
|
||||
if(BooleanUtils.isTrue(skipSomeSteps))
|
||||
{
|
||||
runBackendStepOutput.setOverrideLastStepName(STEP_2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
6
pom.xml
6
pom.xml
@ -29,6 +29,7 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>qqq-bom</module>
|
||||
<module>qqq-backend-core</module>
|
||||
<module>qqq-backend-module-api</module>
|
||||
<module>qqq-backend-module-filesystem</module>
|
||||
@ -45,7 +46,7 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>0.20.0-SNAPSHOT</revision>
|
||||
<revision>0.20.0</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
@ -149,7 +150,7 @@
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>9.0</version>
|
||||
<version>10.16.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
@ -208,6 +209,7 @@
|
||||
<productionBranch>main</productionBranch>
|
||||
<developmentBranch>dev</developmentBranch>
|
||||
<versionTagPrefix>version-</versionTagPrefix>
|
||||
<releaseBranchPrefix>rel/</releaseBranchPrefix>
|
||||
</gitFlowConfig>
|
||||
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
|
||||
<postReleaseGoals>install</postReleaseGoals> <!-- Let CI run deploys -->
|
||||
|
31
qodana.yaml
Normal file
31
qodana.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
projectJDK: 17 #(Applied in CI/CD pipeline)
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-jvm:latest
|
@ -173,12 +173,32 @@
|
||||
<version>1.12.321</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-ses</artifactId>
|
||||
<version>1.12.705</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cloud.localstack</groupId>
|
||||
<artifactId>localstack-utils</artifactId>
|
||||
<version>0.2.20</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.quartz-scheduler</groupId>
|
||||
<artifactId>quartz</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- bring in a newer version of this lib, which quartz transitively loads through c3p0 -->
|
||||
<dependency>
|
||||
<groupId>com.mchange</groupId>
|
||||
<artifactId>mchange-commons-java</artifactId>
|
||||
<version>0.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Many of the deps we bring in use slf4j. This dep maps slf4j to our logger, log4j -->
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
@ -186,6 +206,12 @@
|
||||
<version>2.23.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Common deps for all qqq modules -->
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -55,7 +56,7 @@ public class ActionHelper
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
private static Integer CORE_THREADS = 8;
|
||||
private static Integer MAX_THREADS = 500;
|
||||
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
|
||||
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new PrefixedDefaultThreadFactory(ActionHelper.class));
|
||||
|
||||
|
||||
|
||||
|
@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
|
||||
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.state.StateType;
|
||||
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@ -65,7 +66,7 @@ public class AsyncJobManager
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
private static Integer CORE_THREADS = 8;
|
||||
private static Integer MAX_THREADS = 500;
|
||||
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
|
||||
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new PrefixedDefaultThreadFactory(AsyncJobManager.class));
|
||||
|
||||
|
||||
private String forcedJobUUID = null;
|
||||
|
@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -185,8 +186,7 @@ public class AsyncRecordPipeLoop
|
||||
|
||||
if(recordCount > 0)
|
||||
{
|
||||
LOG.info(String.format("Processed %,d records", recordCount)
|
||||
+ String.format(" at end of job [%s] in %,d ms (%.2f records/second).", jobName, (endTime - jobStartTime), 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
|
||||
LOG.info("End of job summary", logPair("recordCount", recordCount), logPair("jobName", jobName), logPair("millis", endTime - jobStartTime), logPair("recordsPerSecond", 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
|
||||
}
|
||||
|
||||
return (recordCount);
|
||||
|
@ -149,7 +149,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
// sort the field names by their labels //
|
||||
//////////////////////////////////////////
|
||||
List<String> sortedFieldNames = table.getFields().keySet().stream()
|
||||
.sorted(Comparator.comparing(fieldName -> table.getFields().get(fieldName).getLabel()))
|
||||
.sorted(Comparator.comparing(fieldName -> Objects.requireNonNullElse(table.getFields().get(fieldName).getLabel(), fieldName)))
|
||||
.toList();
|
||||
|
||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||
|
@ -127,7 +127,6 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:indentation")
|
||||
public String getInsertOrUpdate()
|
||||
{
|
||||
return switch(this)
|
||||
|
@ -50,10 +50,7 @@ public interface RecordCustomizerUtilityInterface
|
||||
/*******************************************************************************
|
||||
** Container for an old value and a new value.
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:MethodName")
|
||||
record Change(Serializable oldValue, Serializable newValue)
|
||||
{
|
||||
}
|
||||
record Change(Serializable oldValue, Serializable newValue) {}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
@ -161,7 +161,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
|
||||
{
|
||||
String tablePath = QContext.getQInstance().getTablePath(tableName);
|
||||
return (tablePath + "/create?defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
|
||||
return (tablePath + "/create#defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
|
||||
}
|
||||
|
||||
|
||||
@ -183,7 +183,6 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
@ -59,6 +60,7 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -66,6 +68,9 @@ import org.apache.commons.lang.BooleanUtils;
|
||||
*******************************************************************************/
|
||||
public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ChildRecordListRenderer.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -172,126 +177,134 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
String widgetLabel = input.getQueryParams().get("widgetLabel");
|
||||
String joinName = input.getQueryParams().get("joinName");
|
||||
QJoinMetaData join = input.getInstance().getJoin(joinName);
|
||||
String id = input.getQueryParams().get("id");
|
||||
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
|
||||
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
|
||||
|
||||
Integer maxRows = null;
|
||||
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
|
||||
try
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
|
||||
}
|
||||
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
|
||||
}
|
||||
String widgetLabel = input.getQueryParams().get("widgetLabel");
|
||||
String joinName = input.getQueryParams().get("joinName");
|
||||
QJoinMetaData join = input.getInstance().getJoin(joinName);
|
||||
String id = input.getQueryParams().get("id");
|
||||
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
|
||||
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// fetch the record that we're getting children for. e.g., the left-side of the join, with the input id //
|
||||
// but - only try this if we were given an id. note, this widget could be called for on an INSERT screen, where we don't have a record yet //
|
||||
// but we still want to be able to return all the other data in here that otherwise comes from the widget meta data, join, etc. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
int totalRows = 0;
|
||||
QRecord primaryRecord = null;
|
||||
QQueryFilter filter = null;
|
||||
QueryOutput queryOutput = new QueryOutput(new QueryInput());
|
||||
if(StringUtils.hasContent(id))
|
||||
{
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(join.getLeftTable());
|
||||
getInput.setPrimaryKey(id);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
primaryRecord = getOutput.getRecord();
|
||||
|
||||
if(primaryRecord == null)
|
||||
Integer maxRows = null;
|
||||
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
|
||||
}
|
||||
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// set up the query - for the table on the right side of the join //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
filter = new QQueryFilter();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// fetch the record that we're getting children for. e.g., the left-side of the join, with the input id //
|
||||
// but - only try this if we were given an id. note, this widget could be called for on an INSERT screen, where we don't have a record yet //
|
||||
// but we still want to be able to return all the other data in here that otherwise comes from the widget meta data, join, etc. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
int totalRows = 0;
|
||||
QRecord primaryRecord = null;
|
||||
QQueryFilter filter = null;
|
||||
QueryOutput queryOutput = new QueryOutput(new QueryInput());
|
||||
if(StringUtils.hasContent(id))
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
|
||||
}
|
||||
filter.setOrderBys(join.getOrderBys());
|
||||
filter.setLimit(maxRows);
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(join.getLeftTable());
|
||||
getInput.setPrimaryKey(id);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
primaryRecord = getOutput.getRecord();
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getRightTable());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setFilter(filter);
|
||||
queryOutput = new QueryAction().execute(queryInput);
|
||||
if(primaryRecord == null)
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
|
||||
}
|
||||
|
||||
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
|
||||
|
||||
totalRows = queryOutput.getRecords().size();
|
||||
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the input said to only do some max, and the # of results we got is that max, //
|
||||
// then do a count query, for displaying 1-n of <count> //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(join.getRightTable());
|
||||
countInput.setFilter(filter);
|
||||
totalRows = new CountAction().execute(countInput).getCount();
|
||||
}
|
||||
}
|
||||
|
||||
String tablePath = input.getInstance().getTablePath(rightTable.getName());
|
||||
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
|
||||
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
|
||||
|
||||
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
|
||||
{
|
||||
widgetData.setCanAddChildRecord(true);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// new child records must have values from the join-ons //
|
||||
//////////////////////////////////////////////////////////
|
||||
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
|
||||
if(primaryRecord != null)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// set up the query - for the table on the right side of the join //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
filter = new QQueryFilter();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
|
||||
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
|
||||
}
|
||||
filter.setOrderBys(join.getOrderBys());
|
||||
filter.setLimit(maxRows);
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getRightTable());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setFilter(filter);
|
||||
queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
|
||||
|
||||
totalRows = queryOutput.getRecords().size();
|
||||
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the input said to only do some max, and the # of results we got is that max, //
|
||||
// then do a count query, for displaying 1-n of <count> //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(join.getRightTable());
|
||||
countInput.setFilter(filter);
|
||||
totalRows = new CountAction().execute(countInput).getCount();
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
|
||||
String tablePath = input.getInstance().getTablePath(rightTable.getName());
|
||||
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
|
||||
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
|
||||
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
|
||||
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
|
||||
|
||||
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
|
||||
{
|
||||
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are no disabled fields specified - then normally any fields w/ a default value get implicitly disabled //
|
||||
// but - if we didn't look-up the primary record, then we'll want to explicit disable fields from joins //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(primaryRecord == null)
|
||||
widgetData.setCanAddChildRecord(true);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// new child records must have values from the join-ons //
|
||||
//////////////////////////////////////////////////////////
|
||||
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
|
||||
if(primaryRecord != null)
|
||||
{
|
||||
Set<String> implicitlyDisabledFields = new HashSet<>();
|
||||
widgetData.setDisabledFieldsForNewChildRecords(implicitlyDisabledFields);
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
implicitlyDisabledFields.add(joinOn.getRightField());
|
||||
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
|
||||
|
||||
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
|
||||
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
|
||||
{
|
||||
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are no disabled fields specified - then normally any fields w/ a default value get implicitly disabled //
|
||||
// but - if we didn't look-up the primary record, then we'll want to explicit disable fields from joins //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(primaryRecord == null)
|
||||
{
|
||||
Set<String> implicitlyDisabledFields = new HashSet<>();
|
||||
widgetData.setDisabledFieldsForNewChildRecords(implicitlyDisabledFields);
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
implicitlyDisabledFields.add(joinOn.getRightField());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error rendering child record list", e, logPair("widgetName", () -> input.getWidgetMetaData().getName()));
|
||||
throw (e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -256,7 +256,6 @@ public enum DateTimeGroupBy
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:indentation")
|
||||
public Instant roundDown(Instant instant, ZoneId zoneId)
|
||||
{
|
||||
ZonedDateTime zoned = instant.atZone(zoneId);
|
||||
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.AlertData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Widget that can add an Alert to a process screen.
|
||||
**
|
||||
** In the process, you'll want values:
|
||||
** - alertType - name of entry in AlertType enum (ERROR, WARNING, SUCCESS)
|
||||
** - alertHtml - html to display inside the alert (other than its icon)
|
||||
*******************************************************************************/
|
||||
public class ProcessAlertWidget extends AbstractWidgetRenderer implements MetaDataProducerInterface<QWidgetMetaData>
|
||||
{
|
||||
public static final String NAME = "ProcessAlertWidget";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
AlertData.AlertType alertType = AlertData.AlertType.WARNING;
|
||||
if(input.getQueryParams().containsKey("alertType"))
|
||||
{
|
||||
alertType = AlertData.AlertType.valueOf(input.getQueryParams().get("alertType"));
|
||||
}
|
||||
|
||||
String html = "Warning";
|
||||
if(input.getQueryParams().containsKey("alertHtml"))
|
||||
{
|
||||
html = input.getQueryParams().get("alertHtml");
|
||||
}
|
||||
|
||||
return (new RenderWidgetOutput(new AlertData(alertType, html)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QWidgetMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QWidgetMetaData()
|
||||
.withType(WidgetType.ALERT.getType())
|
||||
.withGridColumns(12)
|
||||
.withName(NAME)
|
||||
.withIsCard(false)
|
||||
.withShowReloadButton(false)
|
||||
.withCodeReference(new QCodeReference(getClass()));
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for actions that a backend can perform, based on streaming data
|
||||
** into the backend's storage.
|
||||
** into the backend's storage.
|
||||
*******************************************************************************/
|
||||
public interface QStorageInterface
|
||||
{
|
||||
@ -46,4 +46,24 @@ public interface QStorageInterface
|
||||
*******************************************************************************/
|
||||
InputStream getInputStream(StorageInput storageInput) throws QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void makePublic(StorageInput storageInput) throws QException
|
||||
{
|
||||
//////////
|
||||
// noop //
|
||||
//////////
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default String getDownloadURL(StorageInput storageInput) throws QException
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.messaging;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.messaging.QMessagingProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.messaging.MessagingProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.modules.messaging.QMessagingProviderDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SendMessageAction extends AbstractQActionFunction<SendMessageInput, SendMessageOutput>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public SendMessageOutput execute(SendMessageInput input) throws QException
|
||||
{
|
||||
if(!StringUtils.hasContent(input.getMessagingProviderName()))
|
||||
{
|
||||
throw (new QException("Messaging provider name was not given in SendMessageInput."));
|
||||
}
|
||||
|
||||
QMessagingProviderMetaData messagingProvider = QContext.getQInstance().getMessagingProvider(input.getMessagingProviderName());
|
||||
if(messagingProvider == null)
|
||||
{
|
||||
throw (new QException("Messaging provider named [" + input.getMessagingProviderName() + "] was not found in this QInstance."));
|
||||
}
|
||||
|
||||
MessagingProviderInterface messagingProviderInterface = new QMessagingProviderDispatcher().getMessagingProviderInterface(messagingProvider.getType());
|
||||
|
||||
return (messagingProviderInterface.sendMessage(input));
|
||||
}
|
||||
|
||||
}
|
@ -29,6 +29,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -41,9 +42,19 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
*******************************************************************************/
|
||||
public class JoinGraph
|
||||
{
|
||||
|
||||
private Set<Edge> edges = new HashSet<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// as an instance grows, with the number of joins (say, more than 50?), especially as they may have a lot of connections, //
|
||||
// it can become very very slow to process a full join graph (e.g., 10 seconds, maybe much worse, per Big-O...) //
|
||||
// also, it's not frequently useful to look at a join path that's more than a handful of tables long. //
|
||||
// thus - this property exists - to limit the max length of a join path. Keeping it small keeps instance enrichment //
|
||||
// and validation reasonably performant, at the possible cost of, some join-path that's longer than this limit may not //
|
||||
// be found - but - chances are, you don't want some 12-element join path to be used anyway, thus, this makes sense. //
|
||||
// but - it can be adjusted, per system property or ENV var. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private int maxPathLength = new QMetaDataVariableInterpreter().getIntegerFromPropertyOrEnvironment("qqq.instance.joinGraph.maxPathLength", "QQQ_INSTANCE_JOIN_GRAPH_MAX_PATH_LENGTH", 3);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -303,6 +314,13 @@ public class JoinGraph
|
||||
|
||||
if(otherTableName != null)
|
||||
{
|
||||
if(newPath.size() > maxPathLength)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// performance hack. see comment at maxPathLength definition //
|
||||
////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
JoinConnectionList newConnectionList = connectionList.copy();
|
||||
JoinConnection joinConnection = new JoinConnection(otherTableName, edge.joinName);
|
||||
|
@ -218,6 +218,8 @@ public class MetaDataAction
|
||||
|
||||
metaDataOutput.setEnvironmentValues(metaDataInput.getInstance().getEnvironmentValues());
|
||||
|
||||
metaDataOutput.setHelpContents(metaDataInput.getInstance().getHelpContent());
|
||||
|
||||
// todo post-customization - can do whatever w/ the result if you want?
|
||||
|
||||
return metaDataOutput;
|
||||
|
@ -500,7 +500,6 @@ public class PermissionsHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:indentation")
|
||||
static PermissionSubType getEffectivePermissionSubType(QPermissionRules rules, PermissionSubType originalPermissionSubType)
|
||||
{
|
||||
if(rules == null || rules.getLevel() == null)
|
||||
@ -515,10 +514,10 @@ public class PermissionsHelper
|
||||
if(PrivatePermissionSubType.HAS_ACCESS.equals(originalPermissionSubType))
|
||||
{
|
||||
return switch(rules.getLevel())
|
||||
{
|
||||
case NOT_PROTECTED -> null;
|
||||
default -> PrivatePermissionSubType.HAS_ACCESS;
|
||||
};
|
||||
{
|
||||
case NOT_PROTECTED -> null;
|
||||
default -> PrivatePermissionSubType.HAS_ACCESS;
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -527,30 +526,30 @@ public class PermissionsHelper
|
||||
// permission sub-type to what we expect to be set for the table //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return switch(rules.getLevel())
|
||||
{
|
||||
case NOT_PROTECTED -> null;
|
||||
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
|
||||
case READ_WRITE_PERMISSIONS ->
|
||||
{
|
||||
case NOT_PROTECTED -> null;
|
||||
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
|
||||
case READ_WRITE_PERMISSIONS ->
|
||||
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
|
||||
{
|
||||
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (originalPermissionSubType);
|
||||
}
|
||||
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (PrivatePermissionSubType.WRITE);
|
||||
}
|
||||
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (PrivatePermissionSubType.READ);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
|
||||
}
|
||||
yield (originalPermissionSubType);
|
||||
}
|
||||
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
|
||||
};
|
||||
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (PrivatePermissionSubType.WRITE);
|
||||
}
|
||||
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (PrivatePermissionSubType.READ);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
|
||||
}
|
||||
}
|
||||
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.processes;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.state.StateType;
|
||||
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Action handler for running the cancel step of a qqq process
|
||||
*
|
||||
*******************************************************************************/
|
||||
public class CancelProcessAction extends RunProcessAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CancelProcessAction.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RunProcessOutput execute(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(runProcessInput);
|
||||
|
||||
QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
|
||||
if(process == null)
|
||||
{
|
||||
throw new QBadRequestException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");
|
||||
}
|
||||
|
||||
if(runProcessInput.getProcessUUID() == null)
|
||||
{
|
||||
throw (new QBadRequestException("Cannot cancel process - processUUID was not given."));
|
||||
}
|
||||
|
||||
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
|
||||
Optional<ProcessState> processState = getState(runProcessInput.getProcessUUID());
|
||||
if(processState.isEmpty())
|
||||
{
|
||||
throw (new QBadRequestException("Cannot cancel process - State for process UUID [" + runProcessInput.getProcessUUID() + "] was not found."));
|
||||
}
|
||||
|
||||
RunProcessOutput runProcessOutput = new RunProcessOutput();
|
||||
try
|
||||
{
|
||||
if(process.getCancelStep() != null)
|
||||
{
|
||||
LOG.info("Running cancel step for process", logPair("processName", process.getName()));
|
||||
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, process.getCancelStep(), process, processState.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.debug("Process does not have a custom cancel step to run.", logPair("processName", process.getName()));
|
||||
}
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
////////////////////////////////////////////////////////////
|
||||
// upon exception (e.g., one thrown by a step), throw it. //
|
||||
////////////////////////////////////////////////////////////
|
||||
throw (qe);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error cancelling process", e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
//////////////////////////////////////////////////////
|
||||
// always put the final state in the process result //
|
||||
//////////////////////////////////////////////////////
|
||||
runProcessOutput.setProcessState(processState.get());
|
||||
}
|
||||
|
||||
return (runProcessOutput);
|
||||
}
|
||||
|
||||
}
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -114,4 +115,14 @@ public class QProcessCallbackFactory
|
||||
return (forFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QProcessCallback forPrimaryKeys(String fieldName, Collection<? extends Serializable> values)
|
||||
{
|
||||
return (forFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IN, values))));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
@ -34,6 +35,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
@ -71,7 +73,17 @@ public class RunBackendStepAction
|
||||
QStepMetaData stepMetaData = process.getStep(runBackendStepInput.getStepName());
|
||||
if(stepMetaData == null)
|
||||
{
|
||||
throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]");
|
||||
if(process.getCancelStep() != null && Objects.equals(process.getCancelStep().getName(), runBackendStepInput.getStepName()))
|
||||
{
|
||||
/////////////////////////////////////
|
||||
// special case for cancel step... //
|
||||
/////////////////////////////////////
|
||||
stepMetaData = process.getCancelStep();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
if(!(stepMetaData instanceof QBackendStepMetaData backendStepMetaData))
|
||||
@ -82,7 +94,7 @@ public class RunBackendStepAction
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// ensure input data is set as needed - use callback object to get anything missing //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData);
|
||||
ensureRecordsAreInRequest(runBackendStepInput, backendStepMetaData, process);
|
||||
ensureInputFieldsAreInRequest(runBackendStepInput, backendStepMetaData);
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
@ -167,7 +179,7 @@ public class RunBackendStepAction
|
||||
** check if this step uses a record list - and if so, if we need to get one
|
||||
** via the callback
|
||||
*******************************************************************************/
|
||||
private void ensureRecordsAreInRequest(RunBackendStepInput runBackendStepInput, QBackendStepMetaData step) throws QException
|
||||
private void ensureRecordsAreInRequest(RunBackendStepInput runBackendStepInput, QBackendStepMetaData step, QProcessMetaData process) throws QException
|
||||
{
|
||||
QFunctionInputMetaData inputMetaData = step.getInputMetaData();
|
||||
if(inputMetaData != null && inputMetaData.getRecordListMetaData() != null)
|
||||
@ -190,9 +202,44 @@ public class RunBackendStepAction
|
||||
|
||||
queryInput.setFilter(callback.getQueryFilter());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if process has a max-no of records, set a limit on the process of that number plus 1 //
|
||||
// (the plus 1 being so we can see "oh, you selected more than that many; error!" //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getMaxInputRecords() != null)
|
||||
{
|
||||
if(callback.getQueryFilter() == null)
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter());
|
||||
}
|
||||
|
||||
queryInput.getFilter().setLimit(process.getMaxInputRecords() + 1);
|
||||
}
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
runBackendStepInput.setRecords(queryOutput.getRecords());
|
||||
// todo - handle 0 results found?
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// if process defines a max, and more than the max were found, throw an error //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getMaxInputRecords() != null)
|
||||
{
|
||||
if(queryOutput.getRecords().size() > process.getMaxInputRecords())
|
||||
{
|
||||
throw (new QUserFacingException("Too many records were selected for this process. At most, only " + process.getMaxInputRecords() + " can be selected."));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// if process defines a min, and fewer than the min were found, throw an error //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getMinInputRecords() != null)
|
||||
{
|
||||
if(queryOutput.getRecords().size() < process.getMinInputRecords())
|
||||
{
|
||||
throw (new QUserFacingException("Too few records were selected for this process. At least " + process.getMinInputRecords() + " must be selected."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ public class RunProcessAction
|
||||
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
|
||||
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
|
||||
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
|
||||
public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
|
||||
public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
|
||||
@ -190,7 +190,25 @@ public class RunProcessAction
|
||||
// Run backend steps //
|
||||
///////////////////////
|
||||
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
|
||||
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
|
||||
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the step returned an override lastStepName, use that to determine how we proceed //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getOverrideLastStepName() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
|
||||
lastStepName = runBackendStepOutput.getOverrideLastStepName();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// similarly, if the step produced an updatedFrontendStepList, propagate that data outward //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getUpdatedFrontendStepList() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!");
|
||||
runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -317,6 +335,13 @@ public class RunProcessAction
|
||||
///////////////////////////////////////////////////
|
||||
runProcessInput.seedFromProcessState(optionalProcessState.get());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're restoring an old state, we can discard a previously stored updatedFrontendStepList - //
|
||||
// it is only needed on the transitional edge from a backend-step to a frontend step, but not //
|
||||
// in the other directly //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
optionalProcessState.get().setUpdatedFrontendStepList(null);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if there were values from the caller, put those (back) in the request //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@ -339,7 +364,7 @@ public class RunProcessAction
|
||||
/*******************************************************************************
|
||||
** Run a single backend step.
|
||||
*******************************************************************************/
|
||||
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
|
||||
protected RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
|
||||
{
|
||||
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
|
||||
runBackendStepInput.setProcessName(process.getName());
|
||||
@ -368,14 +393,16 @@ public class RunProcessAction
|
||||
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
|
||||
}
|
||||
|
||||
RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput);
|
||||
storeState(stateKey, lastFunctionResult.getProcessState());
|
||||
RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput);
|
||||
storeState(stateKey, runBackendStepOutput.getProcessState());
|
||||
|
||||
if(lastFunctionResult.getException() != null)
|
||||
if(runBackendStepOutput.getException() != null)
|
||||
{
|
||||
runProcessOutput.setException(lastFunctionResult.getException());
|
||||
throw (lastFunctionResult.getException());
|
||||
runProcessOutput.setException(runBackendStepOutput.getException());
|
||||
throw (runBackendStepOutput.getException());
|
||||
}
|
||||
|
||||
return (runBackendStepOutput);
|
||||
}
|
||||
|
||||
|
||||
@ -495,15 +522,15 @@ public class RunProcessAction
|
||||
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if backend specifies that it uses variants, look for that data in the session and append to our basepull key //
|
||||
// if process specifies that it uses variants, look for that data in the session and append to our basepull key //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getSchedule() != null && process.getVariantBackend() != null)
|
||||
if(process.getVariantBackend() != null)
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
|
||||
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
|
||||
{
|
||||
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
|
||||
LOG.warn("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.queues;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
@ -41,6 +42,8 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSPollerSettings;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
@ -90,15 +93,17 @@ public class SQSQueuePoller implements Runnable
|
||||
}
|
||||
queueUrl += queueMetaData.getQueueName();
|
||||
|
||||
while(true)
|
||||
SQSPollerSettings sqsPollerSettings = getSqsPollerSettings(queueProviderMetaData, queueMetaData);
|
||||
|
||||
for(int loop = 0; loop < sqsPollerSettings.getMaxLoops(); loop++)
|
||||
{
|
||||
///////////////////////////////
|
||||
// fetch a batch of messages //
|
||||
///////////////////////////////
|
||||
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest();
|
||||
receiveMessageRequest.setQueueUrl(queueUrl);
|
||||
receiveMessageRequest.setMaxNumberOfMessages(10);
|
||||
receiveMessageRequest.setWaitTimeSeconds(20); // help urge SQS to query multiple servers and find more messages
|
||||
receiveMessageRequest.setMaxNumberOfMessages(sqsPollerSettings.getMaxNumberOfMessages());
|
||||
receiveMessageRequest.setWaitTimeSeconds(sqsPollerSettings.getWaitTimeSeconds()); // larger value (e.g., 20) can help urge SQS to query multiple servers and find more messages
|
||||
ReceiveMessageResult receiveMessageResult = sqs.receiveMessage(receiveMessageRequest);
|
||||
if(receiveMessageResult.getMessages().isEmpty())
|
||||
{
|
||||
@ -177,6 +182,47 @@ public class SQSQueuePoller implements Runnable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a given queueProvider and queue, get the poller settings to use (using
|
||||
** default values if none are set at either level).
|
||||
*******************************************************************************/
|
||||
static SQSPollerSettings getSqsPollerSettings(SQSQueueProviderMetaData queueProviderMetaData, QQueueMetaData queueMetaData)
|
||||
{
|
||||
/////////////////////////////////
|
||||
// start with default settings //
|
||||
/////////////////////////////////
|
||||
SQSPollerSettings sqsPollerSettings = new SQSPollerSettings()
|
||||
.withMaxLoops(Integer.MAX_VALUE)
|
||||
.withMaxNumberOfMessages(10)
|
||||
.withWaitTimeSeconds(20);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// if the queue provider has settings, let them overwrite defaults //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
if(queueProviderMetaData != null && queueProviderMetaData.getPollerSettings() != null)
|
||||
{
|
||||
SQSPollerSettings providerSettings = queueProviderMetaData.getPollerSettings();
|
||||
sqsPollerSettings.setMaxLoops(Objects.requireNonNullElse(providerSettings.getMaxLoops(), sqsPollerSettings.getMaxLoops()));
|
||||
sqsPollerSettings.setMaxNumberOfMessages(Objects.requireNonNullElse(providerSettings.getMaxNumberOfMessages(), sqsPollerSettings.getMaxNumberOfMessages()));
|
||||
sqsPollerSettings.setWaitTimeSeconds(Objects.requireNonNullElse(providerSettings.getWaitTimeSeconds(), sqsPollerSettings.getWaitTimeSeconds()));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// if the queue has settings, let them overwrite defaults //
|
||||
////////////////////////////////////////////////////////////
|
||||
if(queueMetaData instanceof SQSQueueMetaData sqsQueueMetaData && sqsQueueMetaData.getPollerSettings() != null)
|
||||
{
|
||||
SQSPollerSettings providerSettings = sqsQueueMetaData.getPollerSettings();
|
||||
sqsPollerSettings.setMaxLoops(Objects.requireNonNullElse(providerSettings.getMaxLoops(), sqsPollerSettings.getMaxLoops()));
|
||||
sqsPollerSettings.setMaxNumberOfMessages(Objects.requireNonNullElse(providerSettings.getMaxNumberOfMessages(), sqsPollerSettings.getMaxNumberOfMessages()));
|
||||
sqsPollerSettings.setWaitTimeSeconds(Objects.requireNonNullElse(providerSettings.getWaitTimeSeconds(), sqsPollerSettings.getWaitTimeSeconds()));
|
||||
}
|
||||
|
||||
return sqsPollerSettings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queueProviderMetaData
|
||||
**
|
||||
|
@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@ -216,7 +217,8 @@ public class ExportAction
|
||||
}
|
||||
queryInput.getFilter().setLimit(exportInput.getLimit());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
|
||||
queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
|
||||
queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// tell this query that it needs to put its output into a pipe //
|
||||
|
@ -59,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
|
||||
@ -417,7 +418,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
queryInput.setTableName(dataSource.getSourceTable());
|
||||
queryInput.setFilter(queryFilter);
|
||||
queryInput.setQueryJoins(dataSource.getQueryJoins());
|
||||
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
|
||||
queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
|
||||
queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
|
||||
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
|
||||
@ -457,7 +459,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
if(finalTransformStep != null)
|
||||
{
|
||||
finalTransformStepInput.setRecords(records);
|
||||
finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput);
|
||||
finalTransformStep.runOnePage(finalTransformStepInput, finalTransformStepOutput);
|
||||
records = finalTransformStepOutput.getRecords();
|
||||
}
|
||||
|
||||
@ -557,7 +559,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter)
|
||||
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter) throws QException
|
||||
{
|
||||
if(queryFilter == null || queryFilter.getCriteria() == null)
|
||||
{
|
||||
|
@ -77,7 +77,6 @@ public class ExecuteCodeAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:indentation")
|
||||
public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException
|
||||
{
|
||||
QCodeReference codeReference = input.getCodeReference();
|
||||
|
@ -94,7 +94,7 @@ public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecu
|
||||
protected QRecord buildDetailLogRecord(String logLine)
|
||||
{
|
||||
return (new QRecord()
|
||||
.withValue("scriptLogId", scriptLog.getValue("id"))
|
||||
.withValue("scriptLogId", scriptLog == null ? null : scriptLog.getValue("id"))
|
||||
.withValue("timestamp", Instant.now())
|
||||
.withValue("text", truncate(logLine)));
|
||||
}
|
||||
@ -145,6 +145,14 @@ public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecu
|
||||
{
|
||||
this.executeCodeInput = executeCodeInput;
|
||||
this.scriptLog = buildHeaderRecord(executeCodeInput);
|
||||
|
||||
if(scriptLogLines != null)
|
||||
{
|
||||
for(QRecord scriptLogLine : scriptLogLines)
|
||||
{
|
||||
scriptLogLine.setValue("scriptLogId", scriptLog.getValue("id"));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
@ -22,9 +22,12 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
|
||||
@ -58,6 +61,11 @@ public class AggregateAction
|
||||
QTableMetaData table = aggregateInput.getTable();
|
||||
QBackendMetaData backend = aggregateInput.getBackend();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// apply any available field behaviors to the filter (noting that, if anything changes, a new filter is returned) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
aggregateInput.setFilter(ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getQInstance(), table, aggregateInput.getFilter(), Collections.emptySet()));
|
||||
|
||||
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, aggregateInput.getFilter());
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
@ -67,6 +75,10 @@ public class AggregateAction
|
||||
aggregateInterface.setQueryStat(queryStat);
|
||||
AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput);
|
||||
|
||||
// todo, maybe, not real important? ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.READ, QContext.getQInstance(), table, aggregateOutput.getResults(), null);
|
||||
// issue being, the signature there... it takes a list of QRecords, which aren't what we have...
|
||||
// do we want to ... idk, refactor all these behavior deals? hmm... maybe a new interface/ for ones that do reads? not sure.
|
||||
|
||||
QueryStatManager.getInstance().add(queryStat);
|
||||
|
||||
return aggregateOutput;
|
||||
|
@ -22,9 +22,12 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
@ -58,6 +61,11 @@ public class CountAction
|
||||
QTableMetaData table = countInput.getTable();
|
||||
QBackendMetaData backend = countInput.getBackend();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// apply any available field behaviors to the filter (noting that, if anything changes, a new filter is returned) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
countInput.setFilter(ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getQInstance(), table, countInput.getFilter(), Collections.emptySet()));
|
||||
|
||||
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, countInput.getFilter());
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
|
@ -470,7 +470,7 @@ public class DeleteAction
|
||||
QRecord recordWithError = new QRecord();
|
||||
recordsWithErrors.add(recordWithError);
|
||||
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
|
||||
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + primaryKeyField.getLabel() + " = " + primaryKeyValue));
|
||||
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + Objects.requireNonNullElse(primaryKeyField.getLabel(), primaryKeyField.getName()) + " = " + primaryKeyValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -34,8 +36,10 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.GetActionCacheHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
@ -45,11 +49,16 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldFilterBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -58,20 +67,14 @@ import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
*******************************************************************************/
|
||||
public class GetAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(GetAction.class);
|
||||
|
||||
private Optional<TableCustomizerInterface> postGetRecordCustomizer;
|
||||
|
||||
private GetInput getInput;
|
||||
private QPossibleValueTranslator qPossibleValueTranslator;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QRecord executeForRecord(GetInput getInput) throws QException
|
||||
{
|
||||
return (execute(getInput).getRecord());
|
||||
}
|
||||
private Memoization<Pair<String, String>, List<FieldFilterBehavior<?>>> getFieldFilterBehaviorMemoization = new Memoization<>();
|
||||
|
||||
|
||||
|
||||
@ -108,13 +111,15 @@ public class GetAction
|
||||
}
|
||||
|
||||
GetOutput getOutput;
|
||||
boolean usingDefaultGetInterface = false;
|
||||
boolean usingDefaultGetInterface = false;
|
||||
if(getInterface == null)
|
||||
{
|
||||
getInterface = new DefaultGetInterface();
|
||||
usingDefaultGetInterface = true;
|
||||
}
|
||||
|
||||
getInput = applyFieldBehaviors(getInput);
|
||||
|
||||
getInterface.validateInput(getInput);
|
||||
getOutput = getInterface.execute(getInput);
|
||||
|
||||
@ -140,6 +145,119 @@ public class GetAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private GetInput applyFieldBehaviors(GetInput getInput)
|
||||
{
|
||||
QTableMetaData table = getInput.getTable();
|
||||
|
||||
try
|
||||
{
|
||||
if(getInput.getPrimaryKey() != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the input has a primary key, get its behaviors, then apply, and update the pkey in the input if the value is different //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<FieldFilterBehavior<?>> fieldFilterBehaviors = getFieldFilterBehaviors(table, table.getPrimaryKeyField());
|
||||
for(FieldFilterBehavior<?> fieldFilterBehavior : CollectionUtils.nonNullList(fieldFilterBehaviors))
|
||||
{
|
||||
QFilterCriteria pkeyCriteria = new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey());
|
||||
QFilterCriteria updatedCriteria = ValueBehaviorApplier.apply(pkeyCriteria, QContext.getQInstance(), table, table.getField(table.getPrimaryKeyField()), fieldFilterBehavior);
|
||||
if(updatedCriteria != pkeyCriteria)
|
||||
{
|
||||
getInput.setPrimaryKey(updatedCriteria.getValues().get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(getInput.getUniqueKey() != null)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the input has a unique key, get its behaviors, then apply, and update the ukey values in the input if any are different //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Map<String, Serializable> updatedUniqueKey = new HashMap<>(getInput.getUniqueKey());
|
||||
for(String fieldName : getInput.getUniqueKey().keySet())
|
||||
{
|
||||
List<FieldFilterBehavior<?>> fieldFilterBehaviors = getFieldFilterBehaviors(table, fieldName);
|
||||
for(FieldFilterBehavior<?> fieldFilterBehavior : CollectionUtils.nonNullList(fieldFilterBehaviors))
|
||||
{
|
||||
QFilterCriteria ukeyCriteria = new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, updatedUniqueKey.get(fieldName));
|
||||
QFilterCriteria updatedCriteria = ValueBehaviorApplier.apply(ukeyCriteria, QContext.getQInstance(), table, table.getField(table.getPrimaryKeyField()), fieldFilterBehavior);
|
||||
updatedUniqueKey.put(fieldName, updatedCriteria.getValues().get(0));
|
||||
}
|
||||
}
|
||||
getInput.setUniqueKey(updatedUniqueKey);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error applying field behaviors to get input - will run with original inputs", e);
|
||||
}
|
||||
|
||||
return (getInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<FieldFilterBehavior<?>> getFieldFilterBehaviors(QTableMetaData tableMetaData, String fieldName)
|
||||
{
|
||||
Pair<String, String> key = new Pair<>(tableMetaData.getName(), fieldName);
|
||||
return getFieldFilterBehaviorMemoization.getResult(key, (p) ->
|
||||
{
|
||||
List<FieldFilterBehavior<?>> rs = new ArrayList<>();
|
||||
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(tableMetaData.getFields().get(fieldName).getBehaviors()))
|
||||
{
|
||||
if(fieldBehavior instanceof FieldFilterBehavior<?> fieldFilterBehavior)
|
||||
{
|
||||
rs.add(fieldFilterBehavior);
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** shorthand way to call for the most common use-case, when you just want the
|
||||
** output record to be returned.
|
||||
*******************************************************************************/
|
||||
public QRecord executeForRecord(GetInput getInput) throws QException
|
||||
{
|
||||
return (execute(getInput).getRecord());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** more shorthand way to call for the most common use-case, when you just want the
|
||||
** output record to be returned, and you just want to pass in a table name and primary key.
|
||||
*******************************************************************************/
|
||||
public static QRecord execute(String tableName, Serializable primaryKey) throws QException
|
||||
{
|
||||
GetAction getAction = new GetAction();
|
||||
GetInput getInput = new GetInput(tableName).withPrimaryKey(primaryKey);
|
||||
return getAction.executeForRecord(getInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** more shorthand way to call for the most common use-case, when you just want the
|
||||
** output record to be returned, and you just want to pass in a table name and unique key
|
||||
*******************************************************************************/
|
||||
public static QRecord execute(String tableName, Map<String, Serializable> uniqueKey) throws QException
|
||||
{
|
||||
GetAction getAction = new GetAction();
|
||||
GetInput getInput = new GetInput(tableName).withUniqueKey(uniqueKey);
|
||||
return getAction.executeForRecord(getInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run a GetAction by using the QueryAction instead (e.g., with a filter made
|
||||
** from the pkey/ukey, and returning the single record if found).
|
||||
@ -228,6 +346,8 @@ public class GetAction
|
||||
returnRecord = postGetRecordCustomizer.get().postQuery(getInput, List.of(record)).get(0);
|
||||
}
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.READ, QContext.getQInstance(), getInput.getTable(), List.of(record), null);
|
||||
|
||||
if(getInput.getShouldTranslatePossibleValues())
|
||||
{
|
||||
if(qPossibleValueTranslator == null)
|
||||
|
@ -62,6 +62,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.DuplicateKeyBadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
@ -414,7 +415,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
|
||||
if(keyValues.isPresent() && (existingKeys.get(uniqueKey).contains(keyValues.get()) || keysInThisList.get(uniqueKey).contains(keyValues.get())))
|
||||
{
|
||||
record.addError(new BadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
|
||||
record.addError(new DuplicateKeyBadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
|
||||
foundDupe = true;
|
||||
break;
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -41,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryActionCacheHel
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
@ -117,6 +119,11 @@ public class QueryAction
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// apply any available field behaviors to the filter (noting that, if anything changes, a new filter is returned) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
queryInput.setFilter(ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getQInstance(), table, queryInput.getFilter(), Collections.emptySet()));
|
||||
|
||||
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, queryInput.getFilter());
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
@ -151,6 +158,22 @@ public class QueryAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** shorthand way to call for the most common use-case, when you just want the
|
||||
** records to be returned, and you just want to pass in a table name and filter.
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> execute(String tableName, QQueryFilter filter) throws QException
|
||||
{
|
||||
QueryAction queryAction = new QueryAction();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(filter);
|
||||
QueryOutput queryOutput = queryAction.execute(queryInput);
|
||||
return (queryOutput.getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -169,6 +192,7 @@ public class QueryAction
|
||||
nextLevelQueryInput.setTableName(association.getAssociatedTableName());
|
||||
nextLevelQueryInput.setIncludeAssociations(true);
|
||||
nextLevelQueryInput.setAssociationNamesToInclude(buildNextLevelAssociationNamesToInclude(association.getName(), queryInput.getAssociationNamesToInclude()));
|
||||
nextLevelQueryInput.setTransaction(queryInput.getTransaction());
|
||||
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
nextLevelQueryInput.setFilter(filter);
|
||||
@ -267,6 +291,8 @@ public class QueryAction
|
||||
records = postQueryRecordCustomizer.get().postQuery(queryInput, records);
|
||||
}
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.READ, QContext.getQInstance(), queryInput.getTable(), records, null);
|
||||
|
||||
if(queryInput.getShouldTranslatePossibleValues())
|
||||
{
|
||||
if(qPossibleValueTranslator == null)
|
||||
|
@ -157,6 +157,17 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
output.setDeleteOutput(deleteOutput);
|
||||
}
|
||||
|
||||
if(input.getSetPrimaryKeyInInsertedRecords())
|
||||
{
|
||||
for(int i = 0; i < insertList.size(); i++)
|
||||
{
|
||||
if(i < insertOutput.getRecords().size())
|
||||
{
|
||||
insertList.get(i).setValue(primaryKeyField, insertOutput.getRecords().get(i).getValue(primaryKeyField));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(weOwnTheTransaction)
|
||||
{
|
||||
transaction.commit();
|
||||
|
@ -93,4 +93,28 @@ public class StorageAction
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
|
||||
return (qModule);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void makePublic(StorageInput storageInput) throws QException
|
||||
{
|
||||
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
|
||||
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
|
||||
storageInterface.makePublic(storageInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getDownloadURL(StorageInput storageInput) throws QException
|
||||
{
|
||||
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
|
||||
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
|
||||
return (storageInterface.getDownloadURL(storageInput));
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -68,6 +69,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
@ -334,6 +336,9 @@ public class UpdateAction
|
||||
QTableMetaData table = updateInput.getTable();
|
||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// todo - evolve to use lock tree (e.g., from multi-locks) //
|
||||
/////////////////////////////////////////////////////////////
|
||||
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
|
||||
|
||||
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
|
||||
@ -393,7 +398,12 @@ public class UpdateAction
|
||||
QRecord oldRecord = lookedUpRecords.get(value);
|
||||
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
|
||||
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
|
||||
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
|
||||
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap());
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
errors.forEach(e -> record.addError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -50,6 +52,9 @@ public class ActionTimeoutHelper
|
||||
|
||||
private boolean didTimeout = false;
|
||||
|
||||
private static Integer CORE_THREADS = 10;
|
||||
private static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(CORE_THREADS, new PrefixedDefaultThreadFactory(ActionTimeoutHelper.class));
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -75,7 +80,7 @@ public class ActionTimeoutHelper
|
||||
return;
|
||||
}
|
||||
|
||||
future = Executors.newSingleThreadScheduledExecutor().schedule(() ->
|
||||
future = scheduledExecutorService.schedule(() ->
|
||||
{
|
||||
didTimeout = true;
|
||||
runnable.run();
|
||||
|
@ -57,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@ -176,7 +177,7 @@ public class QueryStatManager
|
||||
active = true;
|
||||
queryStats = new ArrayList<>();
|
||||
|
||||
executorService = Executors.newSingleThreadScheduledExecutor();
|
||||
executorService = Executors.newSingleThreadScheduledExecutor(new PrefixedDefaultThreadFactory(this));
|
||||
executorService.scheduleAtFixedRate(new QueryStatManagerInsertJob(), jobInitialDelay, jobPeriodSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@ -214,71 +215,78 @@ public class QueryStatManager
|
||||
*******************************************************************************/
|
||||
public void add(QueryStat queryStat)
|
||||
{
|
||||
if(queryStat == null)
|
||||
try
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(active)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set fields that we need to capture now (rather than when the thread to store runs) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(queryStat.getFirstResultTimestamp() == null)
|
||||
if(queryStat == null)
|
||||
{
|
||||
queryStat.setFirstResultTimestamp(Instant.now());
|
||||
}
|
||||
|
||||
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
|
||||
{
|
||||
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
|
||||
queryStat.setFirstResultMillis((int) millis);
|
||||
}
|
||||
|
||||
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// discard this record if it's under the min millis setting //
|
||||
//////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
|
||||
if(active)
|
||||
{
|
||||
queryStat.setSessionId(QContext.getQSession().getUuid());
|
||||
}
|
||||
|
||||
if(queryStat.getAction() == null)
|
||||
{
|
||||
if(!QContext.getActionStack().isEmpty())
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set fields that we need to capture now (rather than when the thread to store runs) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(queryStat.getFirstResultTimestamp() == null)
|
||||
{
|
||||
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
|
||||
queryStat.setFirstResultTimestamp(Instant.now());
|
||||
}
|
||||
else
|
||||
|
||||
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
|
||||
{
|
||||
boolean expected = false;
|
||||
Exception e = new Exception("Unexpected empty action stack");
|
||||
for(StackTraceElement stackTraceElement : e.getStackTrace())
|
||||
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
|
||||
queryStat.setFirstResultMillis((int) millis);
|
||||
}
|
||||
|
||||
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// discard this record if it's under the min millis setting //
|
||||
//////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
|
||||
{
|
||||
queryStat.setSessionId(QContext.getQSession().getUuid());
|
||||
}
|
||||
|
||||
if(queryStat.getAction() == null)
|
||||
{
|
||||
if(QContext.getActionStack() != null && !QContext.getActionStack().isEmpty())
|
||||
{
|
||||
String className = stackTraceElement.getClassName();
|
||||
if(className.contains(QueryStatManagerInsertJob.class.getName()))
|
||||
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
|
||||
}
|
||||
else
|
||||
{
|
||||
boolean expected = false;
|
||||
Exception e = new Exception("Unexpected empty action stack");
|
||||
for(StackTraceElement stackTraceElement : e.getStackTrace())
|
||||
{
|
||||
expected = true;
|
||||
break;
|
||||
String className = stackTraceElement.getClassName();
|
||||
if(className.contains(QueryStatManagerInsertJob.class.getName()))
|
||||
{
|
||||
expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!expected)
|
||||
{
|
||||
LOG.debug(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!expected)
|
||||
{
|
||||
LOG.debug(e);
|
||||
}
|
||||
synchronized(this)
|
||||
{
|
||||
queryStats.add(queryStat);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
queryStats.add(queryStat);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error adding query stat", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -49,7 +50,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
@ -82,160 +85,273 @@ public class ValidateRecordSecurityLockHelper
|
||||
*******************************************************************************/
|
||||
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
|
||||
{
|
||||
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
|
||||
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
|
||||
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
|
||||
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we will be relying on primary keys being set in records - but (at least for inserts) //
|
||||
// we might not have pkeys - so make them up (and clear them out at the end) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
Map<Serializable, QRecord> madeUpPrimaryKeys = makeUpPrimaryKeysIfNeeded(records, table);
|
||||
|
||||
////////////////////////////////
|
||||
// actually check lock values //
|
||||
////////////////////////////////
|
||||
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
|
||||
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys);
|
||||
|
||||
/////////////////////////////////
|
||||
// propagate errors to records //
|
||||
/////////////////////////////////
|
||||
for(RecordWithErrors recordWithErrors : errorRecords.values())
|
||||
{
|
||||
recordWithErrors.propagateErrorsToRecord(locksToCheck);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// remove made-up primary keys //
|
||||
/////////////////////////////////
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
for(QRecord record : madeUpPrimaryKeys.values())
|
||||
{
|
||||
record.setValue(primaryKeyField, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a list of `records` from a `table`, and a given `action`, evaluate a
|
||||
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
|
||||
** of `errorRecords` - key'ed by primary key value (real or made up), with
|
||||
** error messages existing in a tree, with positions matching the multi-lock
|
||||
** tree that we're navigating, as tracked by `treePosition`.
|
||||
**
|
||||
** Recursively processes multi-locks (and the top-level call is always with a
|
||||
** multi-lock - as the table's recordLocks list converted to an AND-multi-lock).
|
||||
**
|
||||
** Of note - for the case of READ_WRITE locks, we're only evaluating the values
|
||||
** on the record, to see if they're allowed for us to store (because if we didn't
|
||||
** have the key, we wouldn't have been able to read the value (which is verified
|
||||
** outside of here, in UpdateAction/DeleteAction).
|
||||
**
|
||||
** BUT - WRITE locks - in their case, we read the record no matter what, and in
|
||||
** here we need to verify we have a key that allows us to WRITE the record.
|
||||
*******************************************************************************/
|
||||
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys) throws QException
|
||||
{
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// for multi-locks, make recursive descent //
|
||||
/////////////////////////////////////////////
|
||||
int i = 0;
|
||||
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
|
||||
{
|
||||
treePosition.add(i);
|
||||
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
|
||||
treePosition.remove(treePosition.size() - 1);
|
||||
i++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// actually check lock values //
|
||||
// proceed w/ non-multi locks //
|
||||
////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : locksToCheck)
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// handle the value being in the table we're inserting/updating (e.g., no join) //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// handle the value being in the table we're inserting/updating (e.g., no join) //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
|
||||
|
||||
for(QRecord record : records)
|
||||
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
|
||||
{
|
||||
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
|
||||
// So if we're not updating the security field, then no error can come from it! //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
|
||||
// So if we're not updating the security field, then no error can come from it! //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
Serializable recordSecurityValue = record.getValue(field.getName());
|
||||
validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType(), action);
|
||||
Serializable recordSecurityValue = record.getValue(field.getName());
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
|
||||
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||
{
|
||||
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
|
||||
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
|
||||
|
||||
////////////////////////////////
|
||||
// todo probably, but more... //
|
||||
////////////////////////////////
|
||||
// if(leftMostJoin.getLeftTable().equals(table.getName()))
|
||||
// {
|
||||
// leftMostJoin = leftMostJoin.flip();
|
||||
// }
|
||||
|
||||
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
|
||||
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
|
||||
|
||||
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
|
||||
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
|
||||
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
|
||||
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up a query for joined records //
|
||||
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(leftMostJoin.getLeftTable());
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(filter);
|
||||
|
||||
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
|
||||
for(String joinName : recordSecurityLock.getJoinNameChain())
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up a query for joined records //
|
||||
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(leftMostJoin.getLeftTable());
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(filter);
|
||||
|
||||
for(String joinName : recordSecurityLock.getJoinNameChain())
|
||||
///////////////////////////////////////
|
||||
// we don't need the right-most join //
|
||||
///////////////////////////////////////
|
||||
if(!joinName.equals(rightMostJoin.getName()))
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// we don't need the right-most join //
|
||||
///////////////////////////////////////
|
||||
if(!joinName.equals(rightMostJoin.getName()))
|
||||
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
|
||||
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
|
||||
// also build up the query's sub-filters here (only adding them if they're unique). //
|
||||
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
|
||||
for(QRecord inputRecord : inputRecordPage)
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = new ArrayList<>();
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
|
||||
boolean updatingAnyLockJoinFields = false;
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
|
||||
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
|
||||
inputRecordJoinValues.add(inputRecordValue);
|
||||
|
||||
// if we have a value in this field (and it's not the primary key), then it means we're updating part of the lock
|
||||
if(inputRecordValue != null && !joinOn.getRightField().equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
|
||||
updatingAnyLockJoinFields = true;
|
||||
}
|
||||
|
||||
subFilter.addCriteria(inputRecordValue == null
|
||||
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
|
||||
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
|
||||
// also build up the query's sub-filters here (only adding them if they're unique). //
|
||||
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
|
||||
for(QRecord inputRecord : inputRecordPage)
|
||||
//////////////////////////////////
|
||||
// todo maybe, some version of? //
|
||||
//////////////////////////////////
|
||||
// if(action.equals(Action.UPDATE) && !updatingAnyLockJoinFields && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
|
||||
// {
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// // if this is a read-write lock, then if we have the record, it means we were able to read the record. //
|
||||
// // So if we're not updating the security field, then no error can come from it! //
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// continue;
|
||||
// }
|
||||
|
||||
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = new ArrayList<>();
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
|
||||
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
|
||||
inputRecordJoinValues.add(inputRecordValue);
|
||||
|
||||
subFilter.addCriteria(inputRecordValue == null
|
||||
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
}
|
||||
|
||||
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// only add this sub-filter if it's for a list of keys we haven't seen before //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
filter.addSubFilter(subFilter);
|
||||
}
|
||||
|
||||
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// only add this sub-filter if it's for a list of keys we haven't seen before //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
filter.addSubFilter(subFilter);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
|
||||
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
|
||||
for(QRecord joinRecord : queryOutput.getRecords())
|
||||
{
|
||||
List<Serializable> joinRecordValues = new ArrayList<>();
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
|
||||
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
|
||||
{
|
||||
joinValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
}
|
||||
joinRecordValues.add(joinValue);
|
||||
}
|
||||
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
|
||||
}
|
||||
|
||||
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
|
||||
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
|
||||
for(QRecord joinRecord : queryOutput.getRecords())
|
||||
{
|
||||
List<Serializable> joinRecordValues = new ArrayList<>();
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
|
||||
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
|
||||
{
|
||||
joinValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
}
|
||||
joinRecordValues.add(joinValue);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
|
||||
// isn't allowed. if it is found, then validate its value matches this session's security keys //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
|
||||
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
|
||||
// isn't allowed. if it is found, then validate its value matches this session's security keys //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = entry.getKey();
|
||||
List<QRecord> inputRecords = entry.getValue();
|
||||
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = entry.getKey();
|
||||
List<QRecord> inputRecords = entry.getValue();
|
||||
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
|
||||
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
|
||||
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
|
||||
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
|
||||
{
|
||||
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
|
||||
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
|
||||
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
|
||||
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
|
||||
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
|
||||
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||
{
|
||||
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType(), action);
|
||||
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."));
|
||||
}
|
||||
PermissionDeniedMessage error = new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.");
|
||||
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).add(error, treePosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -247,52 +363,80 @@ public class ValidateRecordSecurityLockHelper
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** for tracking errors, we use primary keys. add "made up" ones to records
|
||||
** if needed (e.g., insert use-case).
|
||||
*******************************************************************************/
|
||||
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
|
||||
private static Map<Serializable, QRecord> makeUpPrimaryKeysIfNeeded(List<QRecord> records, QTableMetaData table)
|
||||
{
|
||||
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
|
||||
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
|
||||
|
||||
recordSecurityLocks = switch(action)
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
|
||||
Integer madeUpPrimaryKey = Integer.MIN_VALUE / 2;
|
||||
for(QRecord record : records)
|
||||
{
|
||||
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
|
||||
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
|
||||
if(record.getValue(primaryKeyField) == null)
|
||||
{
|
||||
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
|
||||
record.setValue(primaryKeyField, madeUpPrimaryKey);
|
||||
madeUpPrimaryKey++;
|
||||
}
|
||||
}
|
||||
return madeUpPrimaryKeys;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a given table & action type, convert the table's record locks to a
|
||||
** MultiRecordSecurityLock, with only the appropriate lock-scopes being included
|
||||
** (e.g., read-locks for selects, write-locks for insert/update/delete).
|
||||
*******************************************************************************/
|
||||
static MultiRecordSecurityLock getRecordSecurityLocks(QTableMetaData table, Action action)
|
||||
{
|
||||
List<RecordSecurityLock> allLocksOnTable = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
|
||||
MultiRecordSecurityLock locksOfType = switch(action)
|
||||
{
|
||||
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLockTree(allLocksOnTable);
|
||||
case SELECT -> RecordSecurityLockFilters.filterForReadLockTree(allLocksOnTable);
|
||||
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
|
||||
};
|
||||
|
||||
if(action.equals(Action.UPDATE))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// todo at some point this seemed right, but now it doesn't - needs work. //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// ////////////////////////////////////////////////////////
|
||||
// // when doing an update, convert all OR's to AND's... //
|
||||
// ////////////////////////////////////////////////////////
|
||||
// updateOperators(locksOfType, MultiRecordSecurityLock.BooleanOperator.AND);
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// if there are no locks, just return //
|
||||
////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks))
|
||||
if(locksOfType == null || CollectionUtils.nullSafeIsEmpty(locksOfType.getLocks()))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// decide if any locks need checked - where one may not need checked if it has an all-access key, and the user has all-access //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
|
||||
{
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
// todo do do!
|
||||
LOG.warn("Totally not ready to handle multiRecordSecurityLock in here!!", new Throwable());
|
||||
continue;
|
||||
}
|
||||
return (locksOfType);
|
||||
}
|
||||
|
||||
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** for a full multi-lock tree, set all of the boolean operators to the specified one.
|
||||
*******************************************************************************/
|
||||
private static void updateOperators(MultiRecordSecurityLock multiLock, MultiRecordSecurityLock.BooleanOperator operator)
|
||||
{
|
||||
multiLock.setOperator(operator);
|
||||
for(RecordSecurityLock childLock : multiLock.getLocks())
|
||||
{
|
||||
if(childLock instanceof MultiRecordSecurityLock childMultiLock)
|
||||
{
|
||||
LOG.trace("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
|
||||
}
|
||||
else
|
||||
{
|
||||
locksToCheck.add(recordSecurityLock);
|
||||
updateOperators(childMultiLock, operator);
|
||||
}
|
||||
}
|
||||
|
||||
return (locksToCheck);
|
||||
}
|
||||
|
||||
|
||||
@ -300,9 +444,9 @@ public class ValidateRecordSecurityLockHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
|
||||
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys)
|
||||
{
|
||||
if(recordSecurityValue == null)
|
||||
if(recordSecurityValue == null || (madeUpPrimaryKeys != null && madeUpPrimaryKeys.containsKey(recordSecurityValue)))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// handle null values - error if the NullValueBehavior is DENY //
|
||||
@ -310,7 +454,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
|
||||
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel)));
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -322,15 +466,305 @@ public class ValidateRecordSecurityLockHelper
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// avoid telling the user a value from a foreign record that they didn't pass in themselves. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record."));
|
||||
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record.")));
|
||||
}
|
||||
else
|
||||
{
|
||||
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel()));
|
||||
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel())));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (Collections.emptyList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Class to track errors that we're associating with a record.
|
||||
**
|
||||
** More complex than it first seems to be needed, because as we're evaluating
|
||||
** locks, we might find some, but based on the boolean condition associated with
|
||||
** them, they might not actually be record-level errors.
|
||||
**
|
||||
** e.g., two locks with an OR relationship - as long as one passes, the record
|
||||
** should have no errors. And so-on through the tree of locks/multi-locks.
|
||||
**
|
||||
** Stores the errors in a tree of ErrorTreeNode objects.
|
||||
**
|
||||
** References into that tree are achieved via a List of Integer called "tree positions"
|
||||
** where each entry in the list denotes the index of the tree node at that level.
|
||||
**
|
||||
** e.g., given this tree:
|
||||
** <pre>
|
||||
** A B
|
||||
** / \ /|\
|
||||
** C D E F G
|
||||
** |
|
||||
** H
|
||||
** </pre>
|
||||
**
|
||||
** The positions of each node would be:
|
||||
** <pre>
|
||||
** A: [0]
|
||||
** B: [1]
|
||||
** C: [0,0]
|
||||
** D: [0,1]
|
||||
** E: [1,0]
|
||||
** F: [1,1]
|
||||
** G: [1,2]
|
||||
** H: [0,1,0]
|
||||
** </pre>
|
||||
*******************************************************************************/
|
||||
static class RecordWithErrors
|
||||
{
|
||||
private QRecord record;
|
||||
private ErrorTreeNode errorTree;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RecordWithErrors(QRecord record)
|
||||
{
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add a list of errors, for a given list of tree positions
|
||||
*******************************************************************************/
|
||||
public void addAll(List<QErrorMessage> recordErrors, List<Integer> treePositions)
|
||||
{
|
||||
if(errorTree == null)
|
||||
{
|
||||
errorTree = new ErrorTreeNode();
|
||||
}
|
||||
|
||||
ErrorTreeNode node = errorTree;
|
||||
for(Integer treePosition : treePositions)
|
||||
{
|
||||
if(node.children == null)
|
||||
{
|
||||
node.children = new ArrayList<>(treePosition);
|
||||
}
|
||||
|
||||
while(treePosition >= node.children.size())
|
||||
{
|
||||
node.children.add(null);
|
||||
}
|
||||
|
||||
if(node.children.get(treePosition) == null)
|
||||
{
|
||||
node.children.set(treePosition, new ErrorTreeNode());
|
||||
}
|
||||
|
||||
node = node.children.get(treePosition);
|
||||
}
|
||||
|
||||
if(node.errors == null)
|
||||
{
|
||||
node.errors = new ArrayList<>();
|
||||
}
|
||||
node.errors.addAll(recordErrors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add a single error to a given tree-position
|
||||
*******************************************************************************/
|
||||
public void add(QErrorMessage error, List<Integer> treePositions)
|
||||
{
|
||||
addAll(List.of(error), treePositions);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** after the tree of errors has been built - walk a lock-tree (locksToCheck)
|
||||
** and resolve boolean operations, to get a final list of errors (possibly empty)
|
||||
** to put on the record.
|
||||
*******************************************************************************/
|
||||
public void propagateErrorsToRecord(MultiRecordSecurityLock locksToCheck)
|
||||
{
|
||||
List<QErrorMessage> errors = recursivePropagation(locksToCheck, new ArrayList<>());
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
errors.forEach(e -> record.addError(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** recursive implementation of the propagation method - e.g., walk tree applying
|
||||
** boolean logic.
|
||||
*******************************************************************************/
|
||||
private List<QErrorMessage> recursivePropagation(MultiRecordSecurityLock locksToCheck, List<Integer> treePositions)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// build a list of errors at this level (and deeper levels too) //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
List<QErrorMessage> errorsFromThisLevel = new ArrayList<>();
|
||||
|
||||
int i = 0;
|
||||
for(RecordSecurityLock lock : locksToCheck.getLocks())
|
||||
{
|
||||
List<QErrorMessage> errorsFromThisLock;
|
||||
|
||||
treePositions.add(i);
|
||||
if(lock instanceof MultiRecordSecurityLock childMultiLock)
|
||||
{
|
||||
errorsFromThisLock = recursivePropagation(childMultiLock, treePositions);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorsFromThisLock = getErrorsFromTree(treePositions);
|
||||
}
|
||||
|
||||
errorsFromThisLevel.addAll(errorsFromThisLock);
|
||||
|
||||
treePositions.remove(treePositions.size() - 1);
|
||||
i++;
|
||||
}
|
||||
|
||||
if(MultiRecordSecurityLock.BooleanOperator.AND.equals(locksToCheck.getOperator()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// for an AND - if there were ANY errors, then return them. //
|
||||
//////////////////////////////////////////////////////////////
|
||||
if(!errorsFromThisLevel.isEmpty())
|
||||
{
|
||||
return (errorsFromThisLevel);
|
||||
}
|
||||
}
|
||||
else // OR
|
||||
{
|
||||
//////////////////////////////////////////////////////////
|
||||
// for an OR - only return if ALL conditions had errors //
|
||||
//////////////////////////////////////////////////////////
|
||||
if(errorsFromThisLevel.size() == locksToCheck.getLocks().size())
|
||||
{
|
||||
return (errorsFromThisLevel); // todo something smarter?
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// else - no errors - empty list //
|
||||
///////////////////////////////////
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QErrorMessage> getErrorsFromTree(List<Integer> treePositions)
|
||||
{
|
||||
ErrorTreeNode node = errorTree;
|
||||
|
||||
for(Integer treePosition : treePositions)
|
||||
{
|
||||
if(node.children == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if(treePosition >= node.children.size())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if(node.children.get(treePosition) == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
node = node.children.get(treePosition);
|
||||
}
|
||||
|
||||
if(node.errors == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return node.errors;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonUtils.toPrettyJson(this);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return "error in toString";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** tree node used by RecordWithErrors
|
||||
*******************************************************************************/
|
||||
static class ErrorTreeNode
|
||||
{
|
||||
private List<QErrorMessage> errors;
|
||||
private ArrayList<ErrorTreeNode> children;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonUtils.toPrettyJson(this);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return "error in toString";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for errors - only here for Jackson/toString
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<QErrorMessage> getErrors()
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for children - only here for Jackson/toString
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ArrayList<ErrorTreeNode> getChildren()
|
||||
{
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,13 +32,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@ -78,44 +77,6 @@ public class QPossibleValueTranslator
|
||||
|
||||
private int maxSizePerPvsCache = 50_000;
|
||||
|
||||
private Map<String, QBackendTransaction> transactionsPerTable = new HashMap<>();
|
||||
|
||||
// todo not commit - remove instance & session - use Context
|
||||
|
||||
|
||||
boolean useTransactionsAsConnectionPool = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QBackendTransaction getTransaction(String tableName)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////
|
||||
// mmm, this does cut down on connections used - //
|
||||
// especially seems helpful in big exports. //
|
||||
// but, let's just start using connection pools instead... //
|
||||
/////////////////////////////////////////////////////////////
|
||||
if(useTransactionsAsConnectionPool)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!transactionsPerTable.containsKey(tableName))
|
||||
{
|
||||
transactionsPerTable.put(tableName, QBackendTransaction.openFor(new InsertInput(tableName)));
|
||||
}
|
||||
|
||||
return (transactionsPerTable.get(tableName));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error opening transaction for table", logPair("tableName", tableName));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -421,7 +382,6 @@ public class QPossibleValueTranslator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:Indentation")
|
||||
private String doFormatPossibleValue(String formatString, List<String> valueFields, Object id, String label)
|
||||
{
|
||||
List<Object> values = new ArrayList<>();
|
||||
@ -560,21 +520,48 @@ public class QPossibleValueTranslator
|
||||
*******************************************************************************/
|
||||
private void primePvsCache(String tableName, List<QPossibleValueSource> possibleValueSources, Collection<Serializable> values)
|
||||
{
|
||||
String idField = null;
|
||||
for(QPossibleValueSource possibleValueSource : possibleValueSources)
|
||||
{
|
||||
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
|
||||
String thisPvsIdField;
|
||||
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
|
||||
{
|
||||
thisPvsIdField = possibleValueSource.getOverrideIdField();
|
||||
}
|
||||
else
|
||||
{
|
||||
thisPvsIdField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
|
||||
}
|
||||
|
||||
if(idField == null)
|
||||
{
|
||||
idField = thisPvsIdField;
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// does this ever happen? maybe not... because, like, the list of values probably wouldn't make sense for //
|
||||
// more than one field in the table... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!idField.equals(thisPvsIdField))
|
||||
{
|
||||
for(QPossibleValueSource valueSource : possibleValueSources)
|
||||
{
|
||||
primePvsCache(tableName, List.of(valueSource), values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
String primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
|
||||
|
||||
for(List<Serializable> page : CollectionUtils.getPages(values, 1000))
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
|
||||
queryInput.setTransaction(getTransaction(tableName));
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(idField, QCriteriaOperator.IN, page)));
|
||||
queryInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels //
|
||||
@ -618,7 +605,7 @@ public class QPossibleValueTranslator
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
Serializable pkeyValue = record.getValue(primaryKeyField);
|
||||
Serializable pkeyValue = record.getValue(idField);
|
||||
for(QPossibleValueSource possibleValueSource : possibleValueSources)
|
||||
{
|
||||
QPossibleValue<?> possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel());
|
||||
|
@ -275,8 +275,19 @@ public class SearchPossibleValueSourceAction
|
||||
|
||||
queryInput.setFilter(queryFilter);
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
String fieldName;
|
||||
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
|
||||
{
|
||||
fieldName = possibleValueSource.getOverrideIdField();
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldName = table.getPrimaryKeyField();
|
||||
}
|
||||
|
||||
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
|
||||
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
|
||||
output.setResults(qPossibleValues);
|
||||
|
||||
|
@ -22,12 +22,18 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.values;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldFilterBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -46,6 +52,7 @@ public class ValueBehaviorApplier
|
||||
{
|
||||
INSERT,
|
||||
UPDATE,
|
||||
READ,
|
||||
FORMATTING
|
||||
}
|
||||
|
||||
@ -97,4 +104,169 @@ public class ValueBehaviorApplier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** apply field behaviors (of FieldFilterBehavior type) to a QQueryFilter.
|
||||
** note that, we don't like to ever edit a QQueryFilter itself (e.g., as it might
|
||||
** have come from meta-data, or it might have some immutable structures in it).
|
||||
** So, if any changes are needed, they'll be returned in a clone.
|
||||
** So, either way, you should use this method like:
|
||||
*
|
||||
** QQueryFilter myFilter = // wherever I got my filter from
|
||||
** myFilter = ValueBehaviorApplier.applyFieldBehaviorsToFilter(QContext.getInstance, table, myFilter, null);
|
||||
** // e.g., always re-assign over top of your filter.
|
||||
*******************************************************************************/
|
||||
public static QQueryFilter applyFieldBehaviorsToFilter(QInstance instance, QTableMetaData table, QQueryFilter filter, Set<FieldBehavior<?>> behaviorsToOmit)
|
||||
{
|
||||
////////////////////////////////////////////////
|
||||
// for null or empty filter, return the input //
|
||||
////////////////////////////////////////////////
|
||||
if(filter == null || !filter.hasAnyCriteria())
|
||||
{
|
||||
return (filter);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// track if we need to make & return a clone. //
|
||||
// which will be the case if we get back any different criteria, //
|
||||
// or any different sub-filters, than what we originally had. //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
boolean needToUseClone = false;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make a new criteria list, and a new subFilter list - either null, if the source was null, or a new array list //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QFilterCriteria> newCriteriaList = filter.getCriteria() == null ? null : new ArrayList<>();
|
||||
List<QQueryFilter> newSubFilters = filter.getSubFilters() == null ? null : new ArrayList<>();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// for each criteria, if its field has any applicable behaviors, apply them //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
for(QFilterCriteria criteria : CollectionUtils.nonNullList(filter.getCriteria()))
|
||||
{
|
||||
QFieldMetaData field = table.getFields().get(criteria.getFieldName());
|
||||
if(field == null && criteria.getFieldName() != null && criteria.getFieldName().contains("."))
|
||||
{
|
||||
String[] parts = criteria.getFieldName().split("\\.");
|
||||
if(parts.length == 2)
|
||||
{
|
||||
QTableMetaData joinTable = instance.getTable(parts[0]);
|
||||
if(joinTable != null)
|
||||
{
|
||||
field = joinTable.getFields().get(parts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QFilterCriteria criteriaToUse = criteria;
|
||||
if(field != null)
|
||||
{
|
||||
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors()))
|
||||
{
|
||||
boolean applyBehavior = true;
|
||||
if(behaviorsToOmit != null && behaviorsToOmit.contains(fieldBehavior))
|
||||
{
|
||||
applyBehavior = false;
|
||||
}
|
||||
|
||||
if(applyBehavior && fieldBehavior instanceof FieldFilterBehavior<?> filterBehavior)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// call to apply the behavior on the criteria - which will return a //
|
||||
// new criteria if any values are changed, else the input criteria //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
criteriaToUse = apply(criteriaToUse, instance, table, field, filterBehavior);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the new criteria is not the same as the old criteria, mark that we need to make and return a clone. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(criteriaToUse != criteria)
|
||||
{
|
||||
needToUseClone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newCriteriaList.add(criteriaToUse);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// similar to above - iterate over the subfilters, making a recursive call, and tracking if we //
|
||||
// got back the same object (in which case, there are no changes, and we don't need to clone), //
|
||||
// or a different object (in which case, we do need a clone, because there were changes). //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
|
||||
{
|
||||
QQueryFilter newSubFilter = applyFieldBehaviorsToFilter(instance, table, subFilter, behaviorsToOmit);
|
||||
if(newSubFilter != subFilter)
|
||||
{
|
||||
newSubFilters.add(newSubFilter);
|
||||
needToUseClone = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
newSubFilters.add(subFilter);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we need to return a clone, then do so, replacing the lists with the ones we built in here //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(needToUseClone)
|
||||
{
|
||||
QQueryFilter cloneFilter = filter.clone();
|
||||
cloneFilter.setCriteria(newCriteriaList);
|
||||
cloneFilter.setSubFilters(newSubFilters);
|
||||
return (cloneFilter);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// else, if no clone needed (e.g., no changes), return the original filter //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
return (filter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QFilterCriteria apply(QFilterCriteria criteria, QInstance instance, QTableMetaData table, QFieldMetaData field, FieldFilterBehavior<?> filterBehavior)
|
||||
{
|
||||
if(criteria == null || CollectionUtils.nullSafeIsEmpty(criteria.getValues()))
|
||||
{
|
||||
return (criteria);
|
||||
}
|
||||
|
||||
List<Serializable> newValues = new ArrayList<>();
|
||||
boolean changedAny = false;
|
||||
|
||||
for(Serializable value : criteria.getValues())
|
||||
{
|
||||
Serializable newValue = filterBehavior.applyToFilterCriteriaValue(value, instance, table, field);
|
||||
if(!Objects.equals(value, newValue))
|
||||
{
|
||||
newValues.add(newValue);
|
||||
changedAny = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
newValues.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if(changedAny)
|
||||
{
|
||||
QFilterCriteria clone = criteria.clone();
|
||||
clone.setValues(newValues);
|
||||
return (clone);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (criteria);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -123,6 +123,18 @@ public class QInstanceEnricher
|
||||
*******************************************************************************/
|
||||
public void enrich()
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// at one point, we did apps later - but - it was possible to put tables in an app's //
|
||||
// sections, but not its children list (enrichApp fixes this by adding such tables to //
|
||||
// the children list) so then when enrichTable runs, it looks for fields that are //
|
||||
// possible-values pointed at tables, for adding LINK adornments - and that could //
|
||||
// cause such links to be omitted, mysteriously! so, do app enrichment before tables. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(qInstance.getApps() != null)
|
||||
{
|
||||
qInstance.getApps().values().forEach(this::enrichApp);
|
||||
}
|
||||
|
||||
if(qInstance.getTables() != null)
|
||||
{
|
||||
qInstance.getTables().values().forEach(this::enrichTable);
|
||||
@ -139,11 +151,6 @@ public class QInstanceEnricher
|
||||
qInstance.getBackends().values().forEach(this::enrichBackend);
|
||||
}
|
||||
|
||||
if(qInstance.getApps() != null)
|
||||
{
|
||||
qInstance.getApps().values().forEach(this::enrichApp);
|
||||
}
|
||||
|
||||
if(qInstance.getReports() != null)
|
||||
{
|
||||
qInstance.getReports().values().forEach(this::enrichReport);
|
||||
@ -384,9 +391,9 @@ public class QInstanceEnricher
|
||||
process.setLabel(nameToLabel(process.getName()));
|
||||
}
|
||||
|
||||
if(process.getStepList() != null)
|
||||
for(QStepMetaData step : CollectionUtils.nonNullMap(process.getAllSteps()).values())
|
||||
{
|
||||
process.getStepList().forEach(this::enrichStep);
|
||||
enrichStep(step);
|
||||
}
|
||||
|
||||
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
|
||||
|
@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpRole;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
@ -104,13 +105,21 @@ public class QInstanceHelpContentManager
|
||||
for(String part : key.split(";"))
|
||||
{
|
||||
String[] parts = part.split(":");
|
||||
nameValuePairs.put(parts[0], parts[1]);
|
||||
if(parts.length > 1)
|
||||
{
|
||||
nameValuePairs.put(parts[0], parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Discarding help content with key that does not contain name:value format", logPair("key", key), logPair("id", record.getValue("id")));
|
||||
}
|
||||
}
|
||||
|
||||
String tableName = nameValuePairs.get("table");
|
||||
String processName = nameValuePairs.get("process");
|
||||
String fieldName = nameValuePairs.get("field");
|
||||
String sectionName = nameValuePairs.get("section");
|
||||
String stepName = nameValuePairs.get("step");
|
||||
String widgetName = nameValuePairs.get("widget");
|
||||
String slotName = nameValuePairs.get("slot");
|
||||
|
||||
@ -141,16 +150,20 @@ public class QInstanceHelpContentManager
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(tableName))
|
||||
{
|
||||
processHelpContentForTable(key, tableName, sectionName, fieldName, roles, helpContent);
|
||||
processHelpContentForTable(key, tableName, sectionName, fieldName, slotName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(processName))
|
||||
{
|
||||
processHelpContentForProcess(key, processName, fieldName, roles, helpContent);
|
||||
processHelpContentForProcess(key, processName, fieldName, stepName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(widgetName))
|
||||
{
|
||||
processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
|
||||
}
|
||||
else if(nameValuePairs.containsKey("instanceLevel"))
|
||||
{
|
||||
processHelpContentForInstance(key, slotName, roles, helpContent);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -163,7 +176,7 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
if(table == null)
|
||||
@ -208,6 +221,24 @@ public class QInstanceHelpContentManager
|
||||
optionalSection.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!StringUtils.hasContent(slotName))
|
||||
{
|
||||
LOG.info("Missing slot name in table-level help content", logPair("key", key));
|
||||
}
|
||||
else
|
||||
{
|
||||
if(helpContent != null)
|
||||
{
|
||||
table.withHelpContent(slotName, helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
table.removeHelpContent(slotName, roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -215,7 +246,7 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForProcess(String key, String processName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForProcess(String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process == null)
|
||||
@ -244,6 +275,30 @@ public class QInstanceHelpContentManager
|
||||
optionalField.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else if(StringUtils.hasContent(stepName))
|
||||
{
|
||||
/////////////////////////////
|
||||
// handle a process screen //
|
||||
/////////////////////////////
|
||||
QFrontendStepMetaData frontendStep = process.getFrontendStep(stepName);
|
||||
|
||||
if(frontendStep == null)
|
||||
{
|
||||
LOG.info("Unrecognized process step in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
frontendStep.withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
frontendStep.removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Unrecognized key format for process help content", logPair("key", key));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -277,6 +332,30 @@ public class QInstanceHelpContentManager
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForInstance(String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
if(!StringUtils.hasContent(slotName))
|
||||
{
|
||||
LOG.info("Missing slot name in instance-level help content", logPair("key", key));
|
||||
}
|
||||
else
|
||||
{
|
||||
if(helpContent != null)
|
||||
{
|
||||
QContext.getQInstance().withHelpContent(slotName, helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
QContext.getQInstance().removeHelpContent(slotName, roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add a help content object to a list - replacing an entry in the list with the
|
||||
** same roles if one is found.
|
||||
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.instances;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Object used to record state of a QInstance having been validated.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum QInstanceValidationState
|
||||
{
|
||||
PENDING,
|
||||
RUNNING,
|
||||
COMPLETE
|
||||
}
|
@ -79,6 +79,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QueueType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||
@ -109,6 +111,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
|
||||
import org.quartz.CronExpression;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -138,19 +141,26 @@ public class QInstanceValidator
|
||||
*******************************************************************************/
|
||||
public void validate(QInstance qInstance) throws QInstanceValidationException
|
||||
{
|
||||
if(qInstance.getHasBeenValidated())
|
||||
if(qInstance.getHasBeenValidated() || qInstance.getValidationIsRunning())
|
||||
{
|
||||
//////////////////////////////////////////
|
||||
// don't re-validate if previously done //
|
||||
//////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// don't re-validate if previously complete or currently running (avoids recursive re-validation chaos!) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
// mark validation as running now //
|
||||
////////////////////////////////////
|
||||
QInstanceValidationKey validationKey = new QInstanceValidationKey();
|
||||
qInstance.setValidationIsRunning(validationKey);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the enricher will build a join graph (if there are any joins). we'd like to only do that //
|
||||
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
JoinGraph joinGraph = null;
|
||||
long start = System.currentTimeMillis();
|
||||
try
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -191,6 +201,9 @@ public class QInstanceValidator
|
||||
validateUniqueTopLevelNames(qInstance);
|
||||
|
||||
runPlugins(QInstance.class, qInstance, qInstance);
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
LOG.info("Validation (and enrichment) performance", logPair("millis", (end - start)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -202,9 +215,22 @@ public class QInstanceValidator
|
||||
throw (new QInstanceValidationException(errors));
|
||||
}
|
||||
|
||||
QInstanceValidationKey validationKey = new QInstanceValidationKey();
|
||||
qInstance.setHasBeenValidated(validationKey);
|
||||
//////////////////////////////
|
||||
// mark validation complete //
|
||||
//////////////////////////////
|
||||
qInstance.setJoinGraph(validationKey, joinGraph);
|
||||
qInstance.setHasBeenValidated(validationKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void revalidate(QInstance qInstance) throws QInstanceValidationException
|
||||
{
|
||||
qInstance.setHasBeenValidated(null);
|
||||
validate(qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -415,11 +441,30 @@ public class QInstanceValidator
|
||||
|
||||
if(queueProvider instanceof SQSQueueProviderMetaData sqsQueueProvider)
|
||||
{
|
||||
if(queueProvider.getType() != null)
|
||||
{
|
||||
assertCondition(queueProvider.getType().equals(QueueType.SQS), "Inconsistent Type/class given for queueProvider: " + name + " (SQSQueueProviderMetaData is not allowed for type " + queueProvider.getType() + ")");
|
||||
}
|
||||
|
||||
assertCondition(StringUtils.hasContent(sqsQueueProvider.getAccessKey()), "Missing accessKey for SQSQueueProvider: " + name);
|
||||
assertCondition(StringUtils.hasContent(sqsQueueProvider.getSecretKey()), "Missing secretKey for SQSQueueProvider: " + name);
|
||||
assertCondition(StringUtils.hasContent(sqsQueueProvider.getBaseURL()), "Missing baseURL for SQSQueueProvider: " + name);
|
||||
assertCondition(StringUtils.hasContent(sqsQueueProvider.getRegion()), "Missing region for SQSQueueProvider: " + name);
|
||||
}
|
||||
else if(queueProvider.getClass().equals(QQueueProviderMetaData.class))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// this just means a subtype wasn't used, so, it should be allowed //
|
||||
// (unless we had a case where a type required a subtype?) //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
else
|
||||
{
|
||||
if(queueProvider.getType() != null)
|
||||
{
|
||||
assertCondition(!queueProvider.getType().equals(QueueType.SQS), "Inconsistent Type/class given for queueProvider: " + name + " (" + queueProvider.getClass().getSimpleName() + " is not allowed for type " + queueProvider.getType() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
runPlugins(QQueueProviderMetaData.class, queueProvider, qInstance);
|
||||
});
|
||||
@ -430,7 +475,27 @@ public class QInstanceValidator
|
||||
qInstance.getQueues().forEach((name, queue) ->
|
||||
{
|
||||
assertCondition(Objects.equals(name, queue.getName()), "Inconsistent naming for queue: " + name + "/" + queue.getName() + ".");
|
||||
assertCondition(qInstance.getQueueProvider(queue.getProviderName()) != null, "Unrecognized queue providerName for queue: " + name);
|
||||
|
||||
QQueueProviderMetaData queueProvider = qInstance.getQueueProvider(queue.getProviderName());
|
||||
if(assertCondition(queueProvider != null, "Unrecognized queue providerName for queue: " + name))
|
||||
{
|
||||
if(queue instanceof SQSQueueMetaData)
|
||||
{
|
||||
assertCondition(queueProvider.getType().equals(QueueType.SQS), "Inconsistent class given for queueMetaData: " + name + " (SQSQueueMetaData is not allowed for queue provider of type " + queueProvider.getType() + ")");
|
||||
}
|
||||
else if(queue.getClass().equals(QQueueMetaData.class))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// this just means a subtype wasn't used, so, it should be //
|
||||
// allowed (unless we had a case where a type required a subtype? //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
else
|
||||
{
|
||||
assertCondition(!queueProvider.getType().equals(QueueType.SQS), "Inconsistent class given for queueProvider: " + name + " (" + queue.getClass().getSimpleName() + " is not allowed for type " + queueProvider.getType() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(StringUtils.hasContent(queue.getQueueName()), "Missing queueName for queue: " + name);
|
||||
if(assertCondition(StringUtils.hasContent(queue.getProcessName()), "Missing processName for queue: " + name))
|
||||
{
|
||||
@ -625,6 +690,11 @@ public class QInstanceValidator
|
||||
supplementalTableMetaData.validate(qInstance, table, this);
|
||||
}
|
||||
|
||||
if(table.getShareableTableMetaData() != null)
|
||||
{
|
||||
table.getShareableTableMetaData().validate(qInstance, table, this);
|
||||
}
|
||||
|
||||
runPlugins(QTableMetaData.class, table, qInstance);
|
||||
});
|
||||
}
|
||||
@ -652,17 +722,20 @@ public class QInstanceValidator
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(exposedJoin.getJoinPath()), joinPrefix + "is missing a joinPath."))
|
||||
{
|
||||
joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
|
||||
|
||||
boolean foundJoinConnection = false;
|
||||
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
|
||||
if(joinGraph != null)
|
||||
{
|
||||
if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
|
||||
joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
|
||||
|
||||
boolean foundJoinConnection = false;
|
||||
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
|
||||
{
|
||||
foundJoinConnection = true;
|
||||
if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
|
||||
{
|
||||
foundJoinConnection = true;
|
||||
}
|
||||
}
|
||||
assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
|
||||
}
|
||||
assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
|
||||
|
||||
assertCondition(!usedJoinPaths.contains(exposedJoin.getJoinPath()), tablePrefix + "has more than one join with the joinPath: " + exposedJoin.getJoinPath());
|
||||
usedJoinPaths.add(exposedJoin.getJoinPath());
|
||||
@ -782,6 +855,10 @@ public class QInstanceValidator
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
|
||||
{
|
||||
String[] split = fieldName.split("\\.");
|
||||
String joinTableName = split[0];
|
||||
String joinFieldName = split[1];
|
||||
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -823,6 +900,8 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(Objects.equals(tmpTable.getName(), joinTableName), prefix + "has a joinNameChain doesn't end in the expected table [" + joinTableName + "] (was: " + tmpTable.getName() + ")");
|
||||
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
}
|
||||
@ -839,6 +918,7 @@ public class QInstanceValidator
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1456,7 +1536,7 @@ public class QInstanceValidator
|
||||
warn("Error loading expectedType for field [" + fieldMetaData.getName() + "] in process [" + processName + "]: " + e.getMessage());
|
||||
}
|
||||
|
||||
validateSimpleCodeReference("Process " + processName + " code reference: ", codeReference, expectedClass);
|
||||
validateSimpleCodeReference("Process " + processName + " code reference:", codeReference, expectedClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1464,6 +1544,14 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
if(process.getCancelStep() != null)
|
||||
{
|
||||
if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName))
|
||||
{
|
||||
validateSimpleCodeReference("Process " + processName + " cancel step code reference: ", process.getCancelStep().getCode(), BackendStep.class);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// if the process has a schedule, make sure required schedule data populated //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
@ -1475,7 +1563,11 @@ public class QInstanceValidator
|
||||
|
||||
if(process.getVariantBackend() != null)
|
||||
{
|
||||
assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
|
||||
if(qInstance.getBackends() != null)
|
||||
{
|
||||
assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
|
||||
}
|
||||
|
||||
assertCondition(process.getVariantRunStrategy() != null, "A variant run strategy was not set for process " + processName + " (which does specify a variant backend)");
|
||||
}
|
||||
else
|
||||
|
@ -79,7 +79,7 @@ public class LogPair
|
||||
}
|
||||
else if(value instanceof LogPair[] subLogPairs)
|
||||
{
|
||||
String subLogPairsString = Arrays.stream(subLogPairs).map(LogPair::toString).collect(Collectors.joining(","));
|
||||
String subLogPairsString = Arrays.stream(subLogPairs).filter(Objects::nonNull).map(LogPair::toString).collect(Collectors.joining(","));
|
||||
valueString = '{' + subLogPairsString + '}';
|
||||
}
|
||||
else if(value instanceof UnsafeSupplier<?, ?> us)
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.logging;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -147,6 +148,28 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public <T extends Throwable> T warnAndThrow(T t, LogPair... logPairs) throws T
|
||||
{
|
||||
warn(t.getMessage(), t, logPairs);
|
||||
throw (t);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public <T extends Throwable> T errorAndThrow(T t, LogPair... logPairs) throws T
|
||||
{
|
||||
error(t.getMessage(), t, logPairs);
|
||||
throw (t);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -595,7 +618,10 @@ public class QLogger
|
||||
{
|
||||
user = session.getUser().getIdReference();
|
||||
}
|
||||
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user));
|
||||
|
||||
LogPair variantsLogPair = getVariantsLogPair(session);
|
||||
|
||||
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user), variantsLogPair);
|
||||
}
|
||||
|
||||
try
|
||||
@ -615,6 +641,38 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static LogPair getVariantsLogPair(QSession session)
|
||||
{
|
||||
LogPair variantsLogPair = null;
|
||||
try
|
||||
{
|
||||
if(session.getBackendVariants() != null)
|
||||
{
|
||||
LogPair[] variants = new LogPair[session.getBackendVariants().size()];
|
||||
|
||||
int i = 0;
|
||||
for(Map.Entry<String, Serializable> entry : session.getBackendVariants().entrySet())
|
||||
{
|
||||
variants[i] = new LogPair(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
variantsLogPair = new LogPair("variants", variants);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
////////////////
|
||||
// leave null //
|
||||
////////////////
|
||||
}
|
||||
return variantsLogPair;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -23,8 +23,8 @@ package com.kingsrook.qqq.backend.core.model;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -42,7 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||
** implement this interface. or, same idea for a QRecordEntity that provides
|
||||
** its own TableMetaData.
|
||||
*******************************************************************************/
|
||||
public interface MetaDataProducerInterface<T extends TopLevelMetaDataInterface>
|
||||
public interface MetaDataProducerInterface<T extends MetaDataProducerOutput>
|
||||
{
|
||||
int DEFAULT_SORT_ORDER = 500;
|
||||
|
||||
|
@ -107,8 +107,10 @@ public class AbstractActionInput
|
||||
/*******************************************************************************
|
||||
** Getter for instance
|
||||
**
|
||||
** Deprecated. Please use QContext.getInstance() instead
|
||||
*******************************************************************************/
|
||||
@JsonIgnore
|
||||
@Deprecated
|
||||
public QInstance getInstance()
|
||||
{
|
||||
return (QContext.getQInstance());
|
||||
@ -119,8 +121,10 @@ public class AbstractActionInput
|
||||
/*******************************************************************************
|
||||
** Getter for session
|
||||
**
|
||||
** Deprecated. Please use QContext.getSession() instead
|
||||
*******************************************************************************/
|
||||
@JsonIgnore
|
||||
@Deprecated
|
||||
public QSession getSession()
|
||||
{
|
||||
return (QContext.getQSession());
|
||||
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class Attachment
|
||||
{
|
||||
private byte[] contents;
|
||||
private String name;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contents
|
||||
*******************************************************************************/
|
||||
public byte[] getContents()
|
||||
{
|
||||
return (this.contents);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contents
|
||||
*******************************************************************************/
|
||||
public void setContents(byte[] contents)
|
||||
{
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contents
|
||||
*******************************************************************************/
|
||||
public Attachment withContents(byte[] contents)
|
||||
{
|
||||
this.contents = contents;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public Attachment withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class Content
|
||||
{
|
||||
private String body;
|
||||
private ContentRole contentRole;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for body
|
||||
*******************************************************************************/
|
||||
public String getBody()
|
||||
{
|
||||
return (this.body);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for body
|
||||
*******************************************************************************/
|
||||
public void setBody(String body)
|
||||
{
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for body
|
||||
*******************************************************************************/
|
||||
public Content withBody(String body)
|
||||
{
|
||||
this.body = body;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contentRole
|
||||
*******************************************************************************/
|
||||
public ContentRole getContentRole()
|
||||
{
|
||||
return (this.contentRole);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contentRole
|
||||
*******************************************************************************/
|
||||
public void setContentRole(ContentRole contentRole)
|
||||
{
|
||||
this.contentRole = contentRole;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contentRole
|
||||
*******************************************************************************/
|
||||
public Content withContentRole(ContentRole contentRole)
|
||||
{
|
||||
this.contentRole = contentRole;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface ContentRole
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
enum Default implements ContentRole
|
||||
{
|
||||
DEFAULT
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class MultiParty extends Party
|
||||
{
|
||||
private List<Party> partyList;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for partyList
|
||||
*******************************************************************************/
|
||||
public List<Party> getPartyList()
|
||||
{
|
||||
return (this.partyList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for partyList
|
||||
*******************************************************************************/
|
||||
public void setPartyList(List<Party> partyList)
|
||||
{
|
||||
this.partyList = partyList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for partyList
|
||||
*******************************************************************************/
|
||||
public MultiParty withPartyList(List<Party> partyList)
|
||||
{
|
||||
this.partyList = partyList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public MultiParty withParty(Party party)
|
||||
{
|
||||
addParty(party);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addParty(Party party)
|
||||
{
|
||||
if(this.partyList == null)
|
||||
{
|
||||
this.partyList = new ArrayList<>();
|
||||
}
|
||||
this.partyList.add(party);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class Party
|
||||
{
|
||||
private String label;
|
||||
private String address;
|
||||
private PartyRole role;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for label
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
*******************************************************************************/
|
||||
public Party withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for address
|
||||
*******************************************************************************/
|
||||
public String getAddress()
|
||||
{
|
||||
return (this.address);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for address
|
||||
*******************************************************************************/
|
||||
public void setAddress(String address)
|
||||
{
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for address
|
||||
*******************************************************************************/
|
||||
public Party withAddress(String address)
|
||||
{
|
||||
this.address = address;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for role
|
||||
*******************************************************************************/
|
||||
public PartyRole getRole()
|
||||
{
|
||||
return (this.role);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for role
|
||||
*******************************************************************************/
|
||||
public void setRole(PartyRole role)
|
||||
{
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for role
|
||||
*******************************************************************************/
|
||||
public Party withRole(PartyRole role)
|
||||
{
|
||||
this.role = role;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface PartyRole
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
enum Default implements PartyRole
|
||||
{
|
||||
DEFAULT
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SendMessageInput extends AbstractActionInput
|
||||
{
|
||||
private String messagingProviderName;
|
||||
private Party to;
|
||||
private Party from;
|
||||
private String subject;
|
||||
private List<Content> contentList;
|
||||
private List<Attachment> attachmentList;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for to
|
||||
*******************************************************************************/
|
||||
public Party getTo()
|
||||
{
|
||||
return (this.to);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for to
|
||||
*******************************************************************************/
|
||||
public void setTo(Party to)
|
||||
{
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for to
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withTo(Party to)
|
||||
{
|
||||
this.to = to;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for from
|
||||
*******************************************************************************/
|
||||
public Party getFrom()
|
||||
{
|
||||
return (this.from);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for from
|
||||
*******************************************************************************/
|
||||
public void setFrom(Party from)
|
||||
{
|
||||
this.from = from;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for from
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withFrom(Party from)
|
||||
{
|
||||
this.from = from;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for subject
|
||||
*******************************************************************************/
|
||||
public String getSubject()
|
||||
{
|
||||
return (this.subject);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for subject
|
||||
*******************************************************************************/
|
||||
public void setSubject(String subject)
|
||||
{
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for subject
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withSubject(String subject)
|
||||
{
|
||||
this.subject = subject;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contentList
|
||||
*******************************************************************************/
|
||||
public List<Content> getContentList()
|
||||
{
|
||||
return (this.contentList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contentList
|
||||
*******************************************************************************/
|
||||
public void setContentList(List<Content> contentList)
|
||||
{
|
||||
this.contentList = contentList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contentList
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withContentList(List<Content> contentList)
|
||||
{
|
||||
this.contentList = contentList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for attachmentList
|
||||
*******************************************************************************/
|
||||
public List<Attachment> getAttachmentList()
|
||||
{
|
||||
return (this.attachmentList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for attachmentList
|
||||
*******************************************************************************/
|
||||
public void setAttachmentList(List<Attachment> attachmentList)
|
||||
{
|
||||
this.attachmentList = attachmentList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for attachmentList
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withAttachmentList(List<Attachment> attachmentList)
|
||||
{
|
||||
this.attachmentList = attachmentList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withContent(Content content)
|
||||
{
|
||||
addContent(content);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addContent(Content content)
|
||||
{
|
||||
if(this.contentList == null)
|
||||
{
|
||||
this.contentList = new ArrayList<>();
|
||||
}
|
||||
this.contentList.add(content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withAttachment(Attachment attachment)
|
||||
{
|
||||
addAttachment(attachment);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addAttachment(Attachment attachment)
|
||||
{
|
||||
if(this.attachmentList == null)
|
||||
{
|
||||
this.attachmentList = new ArrayList<>();
|
||||
}
|
||||
this.attachmentList.add(attachment);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for messagingProviderName
|
||||
*******************************************************************************/
|
||||
public String getMessagingProviderName()
|
||||
{
|
||||
return (this.messagingProviderName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for messagingProviderName
|
||||
*******************************************************************************/
|
||||
public void setMessagingProviderName(String messagingProviderName)
|
||||
{
|
||||
this.messagingProviderName = messagingProviderName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for messagingProviderName
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withMessagingProviderName(String messagingProviderName)
|
||||
{
|
||||
this.messagingProviderName = messagingProviderName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SendMessageOutput extends AbstractActionOutput
|
||||
{
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging.email;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.ContentRole;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum EmailContentRole implements ContentRole
|
||||
{
|
||||
TEXT,
|
||||
HTML
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging.email;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.PartyRole;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum EmailPartyRole implements PartyRole
|
||||
{
|
||||
TO,
|
||||
CC,
|
||||
BCC,
|
||||
FROM,
|
||||
REPLY_TO
|
||||
}
|
@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMe
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -47,8 +48,9 @@ public class MetaDataOutput extends AbstractActionOutput
|
||||
private Map<String, QFrontendWidgetMetaData> widgets;
|
||||
private Map<String, String> environmentValues;
|
||||
|
||||
private List<AppTreeNode> appTree;
|
||||
private QBrandingMetaData branding;
|
||||
private List<AppTreeNode> appTree;
|
||||
private QBrandingMetaData branding;
|
||||
private Map<String, List<QHelpContent>> helpContents;
|
||||
|
||||
|
||||
|
||||
@ -226,4 +228,25 @@ public class MetaDataOutput extends AbstractActionOutput
|
||||
this.environmentValues = environmentValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for helpContents
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setHelpContents(Map<String, List<QHelpContent>> helpContents)
|
||||
{
|
||||
this.helpContents = helpContents;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for helpContents
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Map<String, List<QHelpContent>> getHelpContents()
|
||||
{
|
||||
return helpContents;
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -41,6 +42,11 @@ public class ProcessState implements Serializable
|
||||
private List<String> stepList = new ArrayList<>();
|
||||
private Optional<String> nextStepName = Optional.empty();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -139,4 +145,36 @@ public class ProcessState implements Serializable
|
||||
{
|
||||
this.stepList = stepList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
|
||||
{
|
||||
return (this.updatedFrontendStepList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public ProcessState withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,10 +27,14 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -40,9 +44,13 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
*******************************************************************************/
|
||||
public class RunBackendStepOutput extends AbstractActionOutput implements Serializable
|
||||
{
|
||||
private String processName;
|
||||
|
||||
private ProcessState processState;
|
||||
private Exception exception; // todo - make optional
|
||||
|
||||
private String overrideLastStepName; // todo - does this need to go into state too??
|
||||
|
||||
private List<AuditInput> auditInputList = new ArrayList<>();
|
||||
|
||||
|
||||
@ -78,6 +86,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
|
||||
public void seedFromRequest(RunBackendStepInput runBackendStepInput)
|
||||
{
|
||||
this.processState = runBackendStepInput.getProcessState();
|
||||
this.processName = runBackendStepInput.getProcessName();
|
||||
}
|
||||
|
||||
|
||||
@ -312,4 +321,111 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
|
||||
auditInput.addAuditSingleInput(auditSingleInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for overrideLastStepName
|
||||
*******************************************************************************/
|
||||
public String getOverrideLastStepName()
|
||||
{
|
||||
return (this.overrideLastStepName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for overrideLastStepName
|
||||
*******************************************************************************/
|
||||
public void setOverrideLastStepName(String overrideLastStepName)
|
||||
{
|
||||
this.overrideLastStepName = overrideLastStepName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for overrideLastStepName
|
||||
*******************************************************************************/
|
||||
public RunBackendStepOutput withOverrideLastStepName(String overrideLastStepName)
|
||||
{
|
||||
this.overrideLastStepName = overrideLastStepName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void updateStepList(List<String> stepList)
|
||||
{
|
||||
getProcessState().setStepList(stepList);
|
||||
|
||||
if(processName == null)
|
||||
{
|
||||
throw (new QRuntimeException("ProcessName was not set in this object, therefore updateStepList cannot complete successfully. Try to manually call setProcessName as a work around."));
|
||||
}
|
||||
|
||||
QProcessMetaData processMetaData = QContext.getQInstance().getProcess(processName);
|
||||
|
||||
ArrayList<QFrontendStepMetaData> updatedFrontendStepList = new ArrayList<>(stepList.stream()
|
||||
.map(name -> processMetaData.getStep(name))
|
||||
.filter(step -> step instanceof QFrontendStepMetaData)
|
||||
.map(step -> (QFrontendStepMetaData) step)
|
||||
.toList());
|
||||
|
||||
setUpdatedFrontendStepList(updatedFrontendStepList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for processName
|
||||
*******************************************************************************/
|
||||
public String getProcessName()
|
||||
{
|
||||
return (this.processName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for processName
|
||||
*******************************************************************************/
|
||||
public void setProcessName(String processName)
|
||||
{
|
||||
this.processName = processName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for processName
|
||||
*******************************************************************************/
|
||||
public RunBackendStepOutput withProcessName(String processName)
|
||||
{
|
||||
this.processName = processName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
|
||||
{
|
||||
return (this.processState.getUpdatedFrontendStepList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -327,4 +328,25 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
|
||||
{
|
||||
return this.processState.getUpdatedFrontendStepList();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Information about the query that an application (or qqq service) may know and
|
||||
** want to tell the backend, that can help influence how the backend processes
|
||||
** query.
|
||||
**
|
||||
** For example, a query with potentially a large result set, for MySQL backend,
|
||||
** we may want to configure the result set to stream results rather than do its
|
||||
** default in-memory thing. See RDBMSQueryAction for usage.
|
||||
*******************************************************************************/
|
||||
public enum QueryHint
|
||||
{
|
||||
POTENTIALLY_LARGE_NUMBER_OF_RESULTS,
|
||||
MAY_USE_READ_ONLY_BACKEND
|
||||
}
|
@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
|
||||
@ -44,6 +46,8 @@ public class AggregateInput extends AbstractTableActionInput
|
||||
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
|
||||
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -302,4 +306,78 @@ public class AggregateInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryHints
|
||||
*******************************************************************************/
|
||||
public EnumSet<QueryHint> getQueryHints()
|
||||
{
|
||||
return (this.queryHints);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryHints
|
||||
*******************************************************************************/
|
||||
public void setQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public AggregateInput withQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public AggregateInput withQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
this.queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
}
|
||||
this.queryHints.add(queryHint);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public AggregateInput withoutQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints != null)
|
||||
{
|
||||
this.queryHints.remove(queryHint);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** null-safely check if query hints map contains the specified hint
|
||||
*******************************************************************************/
|
||||
public boolean hasQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (queryHints.contains(queryHint));
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.count;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
|
||||
@ -42,6 +44,8 @@ public class CountInput extends AbstractTableActionInput
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
private Boolean includeDistinctCount = false;
|
||||
|
||||
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -207,4 +211,78 @@ public class CountInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryHints
|
||||
*******************************************************************************/
|
||||
public EnumSet<QueryHint> getQueryHints()
|
||||
{
|
||||
return (this.queryHints);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryHints
|
||||
*******************************************************************************/
|
||||
public void setQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public CountInput withQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public CountInput withQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
this.queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
}
|
||||
this.queryHints.add(queryHint);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public CountInput withoutQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints != null)
|
||||
{
|
||||
this.queryHints.remove(queryHint);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** null-safely check if query hints map contains the specified hint
|
||||
*******************************************************************************/
|
||||
public boolean hasQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (queryHints.contains(queryHint));
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
@ -139,6 +140,24 @@ public class DeleteInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluently add 1 primary key to the delete input
|
||||
**
|
||||
*******************************************************************************/
|
||||
public DeleteInput withPrimaryKey(Serializable primaryKey)
|
||||
{
|
||||
if(primaryKeys == null)
|
||||
{
|
||||
primaryKeys = new ArrayList<>();
|
||||
}
|
||||
|
||||
primaryKeys.add(primaryKey);
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for ids
|
||||
**
|
||||
|
@ -25,10 +25,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
|
||||
@ -60,6 +62,8 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu
|
||||
private boolean includeAssociations = false;
|
||||
private Collection<String> associationNamesToInclude = null;
|
||||
|
||||
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -462,4 +466,79 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryHints
|
||||
*******************************************************************************/
|
||||
public EnumSet<QueryHint> getQueryHints()
|
||||
{
|
||||
return (this.queryHints);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryHints
|
||||
*******************************************************************************/
|
||||
public void setQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public GetInput withQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public GetInput withQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
this.queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
}
|
||||
this.queryHints.add(queryHint);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public GetInput withoutQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints != null)
|
||||
{
|
||||
this.queryHints.remove(queryHint);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** null-safely check if query hints map contains the specified hint
|
||||
*******************************************************************************/
|
||||
public boolean hasQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (queryHints.contains(queryHint));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ public class InsertInput extends AbstractTableActionInput
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InsertInput withRecordEntities(List<QRecordEntity> recordEntityList)
|
||||
public InsertInput withRecordEntities(List<? extends QRecordEntity> recordEntityList)
|
||||
{
|
||||
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
|
||||
{
|
||||
|
@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
@ -550,7 +551,7 @@ public class JoinsContext
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
|
||||
}
|
||||
@ -569,7 +570,7 @@ public class JoinsContext
|
||||
// else, if user/session has some values, build an IN rule - //
|
||||
// noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
|
||||
}
|
||||
|
@ -25,13 +25,18 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.FilterVariableExpression;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -411,6 +416,88 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replaces any FilterVariables' variableNames with one constructed from the field
|
||||
** name, criteria, and index, camel style
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void prepForBackend()
|
||||
{
|
||||
Map<String, Integer> fieldOperatorMap = new HashMap<>();
|
||||
for(QFilterCriteria criterion : getCriteria())
|
||||
{
|
||||
if(criterion.getValues() != null)
|
||||
{
|
||||
int criteriaIndex = 1;
|
||||
int valueIndex = 0;
|
||||
for(Serializable value : criterion.getValues())
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// keep track of what the index is for this criterion, this way if there are //
|
||||
// more than one with the same id/operator values, we can differentiate //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
String backendName = getBackendName(criterion, valueIndex);
|
||||
if(!fieldOperatorMap.containsKey(backendName))
|
||||
{
|
||||
fieldOperatorMap.put(backendName, criteriaIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
criteriaIndex = fieldOperatorMap.get(backendName) + 1;
|
||||
fieldOperatorMap.put(backendName, criteriaIndex);
|
||||
}
|
||||
|
||||
if(value instanceof FilterVariableExpression fve)
|
||||
{
|
||||
if(criteriaIndex > 1)
|
||||
{
|
||||
backendName += criteriaIndex;
|
||||
}
|
||||
fve.setVariableName(backendName);
|
||||
}
|
||||
|
||||
valueIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** builds up a backend name for a field variable expression
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getBackendName(QFilterCriteria criterion, int valueIndex)
|
||||
{
|
||||
StringBuilder backendName = new StringBuilder();
|
||||
for(String fieldNameParts : criterion.getFieldName().split("\\."))
|
||||
{
|
||||
backendName.append(StringUtils.ucFirst(fieldNameParts));
|
||||
}
|
||||
|
||||
for(String operatorParts : criterion.getOperator().name().split("_"))
|
||||
{
|
||||
backendName.append(StringUtils.ucFirst(operatorParts.toLowerCase()));
|
||||
}
|
||||
|
||||
if(criterion.getOperator().equals(QCriteriaOperator.BETWEEN) || criterion.getOperator().equals(QCriteriaOperator.NOT_BETWEEN))
|
||||
{
|
||||
if(valueIndex == 0)
|
||||
{
|
||||
backendName.append("From");
|
||||
}
|
||||
else
|
||||
{
|
||||
backendName.append("To");
|
||||
}
|
||||
}
|
||||
|
||||
return (StringUtils.lcFirst(backendName.toString()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replace any criteria values that look like ${input.XXX} with the value of XXX
|
||||
** from the supplied inputValues map.
|
||||
@ -419,8 +506,10 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
** QQueryFilter - e.g., if it's one that defined in metaData, and that we don't
|
||||
** want to be (permanently) changed!!
|
||||
*******************************************************************************/
|
||||
public void interpretValues(Map<String, Serializable> inputValues)
|
||||
public void interpretValues(Map<String, Serializable> inputValues) throws QException
|
||||
{
|
||||
List<Exception> caughtExceptions = new ArrayList<>();
|
||||
|
||||
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||
variableInterpreter.addValueMap("input", inputValues);
|
||||
for(QFilterCriteria criterion : getCriteria())
|
||||
@ -431,24 +520,45 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
|
||||
for(Serializable value : criterion.getValues())
|
||||
{
|
||||
if(value instanceof AbstractFilterExpression<?>)
|
||||
try
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// todo - do we want to try to interpret values within the expression? //
|
||||
// e.g., greater than now minus ${input.noOfDays} //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
newValues.add(value);
|
||||
if(value instanceof AbstractFilterExpression<?>)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if a filter variable expression, evaluate the input values, which //
|
||||
// will replace the variables with the corresponding actual values //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
if(value instanceof FilterVariableExpression filterVariableExpression)
|
||||
{
|
||||
newValues.add(filterVariableExpression.evaluateInputValues(inputValues));
|
||||
}
|
||||
else
|
||||
{
|
||||
newValues.add(value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String valueAsString = ValueUtils.getValueAsString(value);
|
||||
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
|
||||
newValues.add(interpretedValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
catch(Exception e)
|
||||
{
|
||||
String valueAsString = ValueUtils.getValueAsString(value);
|
||||
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
|
||||
newValues.add(interpretedValue);
|
||||
caughtExceptions.add(e);
|
||||
}
|
||||
}
|
||||
criterion.setValues(newValues);
|
||||
}
|
||||
}
|
||||
|
||||
if(!caughtExceptions.isEmpty())
|
||||
{
|
||||
String message = "Error interpreting filter values: " + StringUtils.joinWithCommasAndAnd(caughtExceptions.stream().map(e -> e.getMessage()).toList());
|
||||
boolean allUserFacing = caughtExceptions.stream().allMatch(QUserFacingException.class::isInstance);
|
||||
throw (allUserFacing ? new QUserFacingException(message) : new QException(message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -25,19 +25,24 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Input data for the Query action
|
||||
**
|
||||
** Todo - maybe make a class between AbstractTableActionInput and {QueryInput,
|
||||
** CountInput, and AggregateInput}, with common attributes for all of these
|
||||
** "read" operations (like, queryHints,
|
||||
*******************************************************************************/
|
||||
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface
|
||||
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface, Cloneable
|
||||
{
|
||||
private QBackendTransaction transaction;
|
||||
private QQueryFilter filter;
|
||||
@ -73,22 +78,6 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Information about the query that an application (or qqq service) may know and
|
||||
** want to tell the backend, that can help influence how the backend processes
|
||||
** query.
|
||||
**
|
||||
** For example, a query with potentially a large result set, for MySQL backend,
|
||||
** we may want to configure the result set to stream results rather than do its
|
||||
** default in-memory thing. See RDBMSQueryAction for usage.
|
||||
*******************************************************************************/
|
||||
public enum QueryHint
|
||||
{
|
||||
POTENTIALLY_LARGE_NUMBER_OF_RESULTS
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -109,6 +98,40 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QueryInput clone() throws CloneNotSupportedException
|
||||
{
|
||||
QueryInput clone = (QueryInput) super.clone();
|
||||
|
||||
if(fieldsToTranslatePossibleValues != null)
|
||||
{
|
||||
clone.fieldsToTranslatePossibleValues = new HashSet<>(fieldsToTranslatePossibleValues);
|
||||
}
|
||||
|
||||
if(queryJoins != null)
|
||||
{
|
||||
clone.queryJoins = new ArrayList<>(queryJoins);
|
||||
}
|
||||
|
||||
if(clone.associationNamesToInclude != null)
|
||||
{
|
||||
clone.associationNamesToInclude = new HashSet<>(associationNamesToInclude);
|
||||
}
|
||||
|
||||
if(queryHints != null)
|
||||
{
|
||||
clone.queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
clone.queryHints.addAll(queryHints);
|
||||
}
|
||||
|
||||
return (clone);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filter
|
||||
**
|
||||
@ -648,4 +671,19 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** null-safely check if query hints map contains the specified hint
|
||||
*******************************************************************************/
|
||||
public boolean hasQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (queryHints.contains(queryHint));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -417,4 +417,20 @@ public class QueryJoin
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "QueryJoin{base="
|
||||
+ baseTableOrAlias + ", joinTable='"
|
||||
+ joinTable + ", joinMetaData="
|
||||
+ (joinMetaData == null ? null : joinMetaData.getName()) + ", alias='"
|
||||
+ alias + ", select="
|
||||
+ select + ", type="
|
||||
+ type + '}';
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
|
||||
}
|
||||
else
|
||||
{
|
||||
storage = new QueryOutputList();
|
||||
storage = new QueryOutputList(queryInput);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -33,15 +36,50 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
*******************************************************************************/
|
||||
class QueryOutputList implements QueryOutputStorageInterface
|
||||
{
|
||||
private List<QRecord> records = new ArrayList<>();
|
||||
private static final QLogger LOG = QLogger.getLogger(QueryOutputList.class);
|
||||
|
||||
private final String tableName;
|
||||
private List<QRecord> records = new ArrayList<>();
|
||||
|
||||
private static int LOG_SIZE_INFO_OVER = 50_000;
|
||||
private static int LOG_SIZE_WARN_OVER = 100_000;
|
||||
private static int LOG_SIZE_ERROR_OVER = 250_000;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QueryOutputList()
|
||||
public QueryOutputList(QueryInput queryInput)
|
||||
{
|
||||
tableName = queryInput.getTableName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void logSize(int sizeBefore, int sizeAfter)
|
||||
{
|
||||
Level level = null;
|
||||
if(sizeBefore < LOG_SIZE_ERROR_OVER && sizeAfter >= LOG_SIZE_ERROR_OVER)
|
||||
{
|
||||
level = Level.ERROR;
|
||||
}
|
||||
else if(sizeBefore < LOG_SIZE_WARN_OVER && sizeAfter >= LOG_SIZE_WARN_OVER)
|
||||
{
|
||||
level = Level.WARN;
|
||||
}
|
||||
else if(sizeBefore < LOG_SIZE_INFO_OVER && sizeAfter >= LOG_SIZE_INFO_OVER)
|
||||
{
|
||||
level = Level.INFO;
|
||||
}
|
||||
|
||||
if(level != null)
|
||||
{
|
||||
LOG.log(level, "Large number of records in QueryOutputList", new Throwable(), logPair("noRecords", sizeAfter), logPair("tableName", tableName));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -52,7 +90,9 @@ class QueryOutputList implements QueryOutputStorageInterface
|
||||
@Override
|
||||
public void addRecord(QRecord record)
|
||||
{
|
||||
int sizeBefore = this.records.size();
|
||||
records.add(record);
|
||||
logSize(sizeBefore, this.records.size());
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +103,9 @@ class QueryOutputList implements QueryOutputStorageInterface
|
||||
@Override
|
||||
public void addRecords(List<QRecord> records)
|
||||
{
|
||||
int sizeBefore = this.records.size();
|
||||
this.records.addAll(records);
|
||||
logSize(sizeBefore, this.records.size());
|
||||
}
|
||||
|
||||
|
||||
@ -77,4 +119,36 @@ class QueryOutputList implements QueryOutputStorageInterface
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for LOG_SIZE_INFO_OVER
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setLogSizeInfoOver(int logSizeInfoOver)
|
||||
{
|
||||
QueryOutputList.LOG_SIZE_INFO_OVER = logSizeInfoOver;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for LOG_SIZE_WARN_OVER
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setLogSizeWarnOver(int logSizeWarnOver)
|
||||
{
|
||||
QueryOutputList.LOG_SIZE_WARN_OVER = logSizeWarnOver;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for LOG_SIZE_ERROR_OVER
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setLogSizeErrorOver(int logSizeErrorOver)
|
||||
{
|
||||
QueryOutputList.LOG_SIZE_ERROR_OVER = logSizeErrorOver;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -33,7 +35,17 @@ public abstract class AbstractFilterExpression<T extends Serializable> implement
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract T evaluate();
|
||||
public abstract T evaluate() throws QException;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T evaluateInputValues(Map<String, Serializable> inputValues) throws QException
|
||||
{
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,214 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class FilterVariableExpression extends AbstractFilterExpression<Serializable>
|
||||
{
|
||||
private String variableName;
|
||||
private String fieldName;
|
||||
private String operator;
|
||||
private int valueIndex = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Serializable evaluate() throws QException
|
||||
{
|
||||
throw (new QUserFacingException("Missing variable value."));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Serializable evaluateInputValues(Map<String, Serializable> inputValues) throws QException
|
||||
{
|
||||
if(!inputValues.containsKey(variableName) || "".equals(ValueUtils.getValueAsString(inputValues.get(variableName))))
|
||||
{
|
||||
throw (new QUserFacingException("Missing value for variable: " + variableName));
|
||||
}
|
||||
return (inputValues.get(variableName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private FilterVariableExpression(String fieldName, int valueIndex)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
this.valueIndex = valueIndex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for valueIndex
|
||||
*******************************************************************************/
|
||||
public int getValueIndex()
|
||||
{
|
||||
return (this.valueIndex);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for valueIndex
|
||||
*******************************************************************************/
|
||||
public void setValueIndex(int valueIndex)
|
||||
{
|
||||
this.valueIndex = valueIndex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for valueIndex
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withValueIndex(int valueIndex)
|
||||
{
|
||||
this.valueIndex = valueIndex;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldName
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return (this.fieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldName
|
||||
*******************************************************************************/
|
||||
public void setFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldName
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variableName
|
||||
*******************************************************************************/
|
||||
public String getVariableName()
|
||||
{
|
||||
return (this.variableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variableName
|
||||
*******************************************************************************/
|
||||
public void setVariableName(String variableName)
|
||||
{
|
||||
this.variableName = variableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variableName
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withVariableName(String variableName)
|
||||
{
|
||||
this.variableName = variableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for operator
|
||||
*******************************************************************************/
|
||||
public String getOperator()
|
||||
{
|
||||
return (this.operator);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for operator
|
||||
*******************************************************************************/
|
||||
public void setOperator(String operator)
|
||||
{
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for operator
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withOperator(String operator)
|
||||
{
|
||||
this.operator = operator;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -35,7 +36,7 @@ public class Now extends AbstractFilterExpression<Instant>
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate()
|
||||
public Instant evaluate() throws QException
|
||||
{
|
||||
return (Instant.now());
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -119,7 +120,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate()
|
||||
public Instant evaluate() throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
|
||||
|
@ -28,6 +28,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
@ -95,7 +96,7 @@ public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate()
|
||||
public Instant evaluate() throws QException
|
||||
{
|
||||
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
|
||||
|
||||
|
@ -39,8 +39,9 @@ public class ReplaceInput extends AbstractTableActionInput
|
||||
private UniqueKey key;
|
||||
private List<QRecord> records;
|
||||
private QQueryFilter filter;
|
||||
private boolean performDeletes = true;
|
||||
private boolean allowNullKeyValuesToEqual = false;
|
||||
private boolean performDeletes = true;
|
||||
private boolean allowNullKeyValuesToEqual = false;
|
||||
private boolean setPrimaryKeyInInsertedRecords = false;
|
||||
|
||||
private boolean omitDmlAudit = false;
|
||||
|
||||
@ -271,4 +272,35 @@ public class ReplaceInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for setPrimaryKeyInInsertedRecords
|
||||
*******************************************************************************/
|
||||
public boolean getSetPrimaryKeyInInsertedRecords()
|
||||
{
|
||||
return (this.setPrimaryKeyInInsertedRecords);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for setPrimaryKeyInInsertedRecords
|
||||
*******************************************************************************/
|
||||
public void setSetPrimaryKeyInInsertedRecords(boolean setPrimaryKeyInInsertedRecords)
|
||||
{
|
||||
this.setPrimaryKeyInInsertedRecords = setPrimaryKeyInInsertedRecords;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for setPrimaryKeyInInsertedRecords
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withSetPrimaryKeyInInsertedRecords(boolean setPrimaryKeyInInsertedRecords)
|
||||
{
|
||||
this.setPrimaryKeyInInsertedRecords = setPrimaryKeyInInsertedRecords;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public UpdateInput withRecordEntities(List<QRecordEntity> recordEntityList)
|
||||
public UpdateInput withRecordEntities(List<? extends QRecordEntity> recordEntityList)
|
||||
{
|
||||
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user