Compare commits

..

115 Commits

Author SHA1 Message Date
31edb6a7fe Merge branch 'rel/0.20.0' 2024-07-06 11:37:14 -05:00
338670118d Update versions for release 2024-07-06 11:29:38 -05:00
32573bdf78 Add releaseBranchPrefix=rel/ (to work around/with release branch exiting now) 2024-07-06 11:26:00 -05:00
bd683253a5 Update qqq-frontend-material-dashboard to 0.20.0 release 2024-07-06 11:13:34 -05:00
75279c2e6c Updated to ingore local cache and additional local generated ENV files with secerts in them 2024-07-06 10:15:28 -05:00
9e33ac564d Adding support for local qodana static analysis within IntelliJ 2024-07-06 10:14:38 -05:00
7fe7c2d0a0 Setup branches for security and release - to work as staging workflow branches for now 2024-07-06 10:11:11 -05:00
79b9f0e921 Setup branches for security and release - to work as staging workflow branches for now 2024-07-06 10:10:56 -05:00
a75ec9a0c5 Merge pull request #111 from Kingsrook/convert-kingsrook-qqq-to-actions-20240706-130034
Convert Kingsrook/qqq to GitHub Actions
2024-07-06 09:49:42 -05:00
b11f1fb394 Create codacy.yml 2024-07-06 09:23:02 -05:00
3c927693f1 Create codeql.yml 2024-07-06 09:21:06 -05:00
cb41f239b8 Create SECURITY.md 2024-07-06 09:18:47 -05:00
8648c67a98 Add composite action upload_docs_site 2024-07-06 08:00:41 -05:00
d11ae90ad6 Add composite action run_asciidoctor 2024-07-06 08:00:40 -05:00
8395dfaa52 Add composite action install_asciidoctor 2024-07-06 08:00:39 -05:00
3273e56b17 Add composite action mvn_jar_deploy 2024-07-06 08:00:39 -05:00
9b1786dc01 Add workflow Kingsrook/qqq/deploy 2024-07-06 08:00:38 -05:00
429513f337 Add composite action store_jacoco_site 2024-07-06 08:00:37 -05:00
6ee8dad45f Add composite action mvn_verify 2024-07-06 08:00:36 -05:00
f380d44dd2 Add composite action install_java17 2024-07-06 08:00:36 -05:00
2cf14e543c Add workflow Kingsrook/qqq/test_only 2024-07-06 08:00:35 -05:00
1669741d19 Add system property picocli.ansi=false, to help reliability of tests, which can fail if the don't expect ansci codes in the outputs 2024-07-05 20:26:06 -05:00
2be41d8714 Update README.md
Corrected Copyright Dates
2024-07-05 13:51:32 -05:00
3e7e416a2a Merge pull request #104 from Kingsrook/feature/CE-1406-item-syncing-story-between
CE-1406 add overridable point: extractSourceKeyValueFromRecord
2024-07-03 16:49:00 -05:00
b377af846a Merge pull request #101 from Kingsrook/feature/CE-1402-field-case-change-behaviors
Feature/ce 1402 field case change behaviors
2024-07-03 16:27:54 -05:00
6b5b971368 CE-1406 add overridable point: extractSourceKeyValueFromRecord 2024-07-03 09:41:46 -05:00
f6e09f1d57 Restore coverage.instructionCoveredRatioMinimum to 80 2024-07-01 08:46:38 -05:00
f069358764 Merge pull request #103 from Kingsrook/feature/fix-c3p0-mysql-result-set-optimization
Fix to use mysqlResultSetOptimization with c3p0-provided connections.
2024-06-28 10:20:41 -05:00
c509b6da38 Fix to use mysqlResultSetOptimization with c3p0-provided connections. 2024-06-28 09:10:16 -05:00
e788929d67 Merge pull request #102 from Kingsrook/feature/cleanups-20240627
Feature/cleanups 20240627
2024-06-27 13:48:15 -05:00
18c94943cb Enrich apps before tables - fixed a situation where a table's possible-value field wasn't getting set up as LINK adornment, due to table not being put in app's child-list, which enrichment does, so, if we enrich app first, it fixed it 2024-06-27 11:52:51 -05:00
b24a990043 Make end-of-job log message use log pairs 2024-06-27 11:51:10 -05:00
a4295df20d Mark getInstance and getSession as deprecated - they already just return value from QContext, but callers should instead do that directly. 2024-06-27 11:50:58 -05:00
3398b812ce Add logging at various increasing levels if more and more records get added to a QueryOutputList 2024-06-27 11:50:37 -05:00
9e9f266878 CE-1402 Add CaseChangeBehavior sub-section 2024-06-25 13:31:13 -05:00
7cbd6705e1 CE-1402 Fix (with test) applying field filter behaviors 2024-06-25 10:32:33 -05:00
1eb078d916 CE-1402 avoid NPE getting behaviors 2024-06-25 10:31:56 -05:00
82201286d4 CE-1402 Make consistent naming 'behaviors', not 'fieldBehaviors' 2024-06-25 08:40:51 -05:00
dc84a9ef55 CE-1402 add instance validation to CaseChangeBehavior 2024-06-25 08:15:59 -05:00
b2cf1cc83b CE-1402 New CaseChangeBehavior, and adding field behaviors to read operations (mostly) and filters and frontend if so specified 2024-06-24 16:09:53 -05:00
848353d804 Merge pull request #100 from Kingsrook/feature/sqs-max-loops
Feature/sqs max loops
2024-06-21 08:29:06 -05:00
e8978a7f92 Add some validation about queue types and classes for meta-data 2024-06-20 16:51:00 -05:00
9d24e61949 Add option to break sqsPoller loop after a given number of iterations - and to config this, plus maxNumberOfMessages and waitTime on a per-queue and/or per-provider level 2024-06-20 16:44:14 -05:00
c748977a1b Validate that an api table doesn't have more than 1 field with the same name (which can happen if it's in the removed set along with main set) 2024-06-19 16:50:00 -05:00
fcae58168e Update to avoid stack-overflow if validation causes validation to happen again 2024-06-19 16:49:04 -05:00
564a5e1095 Avoid NPE if adding a log line before the script header is set 2024-06-19 16:48:05 -05:00
3d7df30db4 Merged feature/CE-1113-single-carton-reqs into dev 2024-06-19 16:05:16 -05:00
a63d5d0c65 Merged feature/system-user-session into dev 2024-06-19 16:02:53 -05:00
e923ce1013 Merged feature/thread-pool-improvements into dev 2024-06-19 16:02:43 -05:00
231ca165a1 Merged feature/rdbms-connection-pool into dev 2024-06-19 16:02:26 -05:00
35c1150f80 Throw explicit exception if backend is missing a name 2024-06-19 15:59:02 -05:00
37f89110e7 Fix to escapeIdentifiers in doDeleteList 2024-06-17 11:00:17 -05:00
4766d2440d ListBuilder.of instead of List.of, for null-tolerance 2024-06-14 10:02:09 -05:00
ad07f3e1f8 CE-1113 Avoid an NPE in doSpecHtml, if branding doesn't have an accent color 2024-06-14 08:59:04 -05:00
7ab2f332e9 CE-1113 Add Map of HelpContent at instance and table levels 2024-06-14 08:57:24 -05:00
00a3f6632c CE-1113 Add ApiFieldCustomValueMapperBulkSupportInterface 2024-06-14 08:56:48 -05:00
9afbe7c3a9 Add formal concept of system-user-session, as a subclass of QSession. during construction, it is given all all-access keys, and its hasPermission method is hard-coded to return true. 2024-06-07 16:52:01 -05:00
27682870a1 Update API query & get with MAY_USE_READ_ONLY query hint (adding that option to Get's) 2024-06-07 16:22:56 -05:00
98031b53cb Give these tests a little room for timing-based instabilitiy 2024-06-07 12:53:26 -05:00
e1fd6d51c4 Update the processes to appear in the instance twice - on both jobs & triggers tables. 2024-06-07 12:46:32 -05:00
0fd2430866 Update to avoid NPE for null backend names (used by some non-standard flows) 2024-06-07 12:43:26 -05:00
9e9d1960c8 rather than exclusion for mchange-common (lib for c3p0), import the version that rdbms will want directly in backend-core 2024-06-06 19:45:12 -05:00
64380dd849 Exclude mchange-commons-java from backend-core -- prefer our own. 2024-06-06 16:41:55 -05:00
c21e9131a0 Update to use PrefixedDefaultThreadFactory, to give threads more unique names (based on service that owns thread pool) 2024-06-06 08:16:33 -05:00
ec04986434 Fix to use a static threadPool, rather than one per each instance! 2024-06-06 08:15:41 -05:00
f7d217a126 Initial checkin 2024-06-06 08:15:12 -05:00
f9cca885ed checkpoint - working version of c3p0 connection pooling, and read-only database meta-data connections (per query hint) 2024-06-05 15:23:02 -05:00
64f706a98e Merged dev into feature/rdbms-connection-pool 2024-06-04 20:06:50 -05:00
7ff7ae3a0c Merge pull request #95 from Kingsrook/feature/CE-938-order-release-automation
Feature/ce 938 order release automation
2024-06-04 20:04:42 -05:00
d7e295881f Merged dev into feature/CE-938-order-release-automation 2024-06-04 19:55:25 -05:00
9fd55746ca Merge pull request #94 from Kingsrook/feature/rename-run-to-runOnePage
Rename 'run' to 'runOnePage'
2024-06-04 19:54:57 -05:00
121f9aa477 Merge pull request #93 from Kingsrook/feature/sprint-43-cleanup
Feature/sprint 43 cleanup
2024-06-04 19:54:46 -05:00
c8edd14833 Merge pull request #92 from Kingsrook/feature/checkstyle-indentation-enhanced-switch
Upgrade checkstyle; remove supressed indentation markers for new-styl…
2024-06-04 19:54:24 -05:00
f7b6028ba1 CE-938: updated to get filter and column setup values from widget data for saved repoprts 2024-06-04 13:45:36 -05:00
bf2836b69f CE-938 - Add test on ProcessAlertWidget 2024-06-04 11:15:16 -05:00
d3f3e25ed5 Checkstyle 2024-06-04 11:07:05 -05:00
d0839dc93c CE-938 - Fix to enrich optional steps too 2024-06-04 11:02:40 -05:00
5540f85466 CE-938 - Initial checkin 2024-06-04 10:58:36 -05:00
1a66f45425 CE-938 - MORE propagation of updated steps from inside streamed-ETL pseudo-steps to top-level process state 2024-06-04 10:58:28 -05:00
90ac1bb9c3 CE-938 - Add existingLock to UnableToObtainProcessLockException 2024-06-04 10:57:52 -05:00
faafacc722 CE-938: fixed bug on deletion final associated child record was not working properly 2024-06-03 15:26:40 -05:00
4508dea767 CE-938 - avoid NPE in release if null input 2024-05-31 11:16:19 -05:00
7c6c02ab28 Add "convenience wrappers" 2024-05-31 11:16:07 -05:00
e1a63752ca (finally) downgrade some of the every-freaking-request session logging 2024-05-31 11:15:16 -05:00
059b746597 CE-938 Fix linkTableCreateWithDefaultValues - needed # not ? 2024-05-31 11:12:24 -05:00
11e1fb86b2 CE-938 move updatedFrontendStepList into state 2024-05-29 10:17:43 -05:00
eb8bf12047 CE-938 Adding cancel-process action, cancelStep meta-data 2024-05-28 16:59:09 -05:00
66b2b4ff4c CE-938 More flexible check in (can update message and control expires-timestamp) 2024-05-24 16:59:56 -05:00
a6e0741175 CE-938: added new general process utils 2024-05-24 16:15:30 -05:00
2b7432167d Actually add the @QIgnore annotation :) 2024-05-23 13:00:19 -05:00
0a12c76829 Add withRecordLabelFormatAndFields to continue to make table-meta-data setup slightly less verbose 2024-05-23 12:56:49 -05:00
3fe6828550 Apply @QIgnore annotation, to silence some debug logs done as part of entity annotaiton processing 2024-05-23 12:56:22 -05:00
27a17183ae CE-938: renamed reportSetup widget to filterAndColumns 2024-05-22 16:23:25 -05:00
94bf10fe6e CE-938 Adding some null-tolerance 2024-05-22 12:05:26 -05:00
610915bf94 CE-938 Committed too early last time 2024-05-21 12:18:59 -05:00
3e26ea94ee CE-938 Make session & user explicit fields, instead of packing into "holder" 2024-05-21 11:35:00 -05:00
e6190b4fe2 CE-938 Add getErrorsAsString; getWarningsAsString; withWarning 2024-05-20 11:35:18 -05:00
1c582621aa CE-938 Add releaseById; Remove throws from release methods (so you don't always have to try-catch yourself); more robust holder processing 2024-05-20 11:34:57 -05:00
b91da93858 CE-938 Add missing javadoc 2024-05-19 20:29:25 -05:00
522dafca69 CE-938 Initial checkin of ProcessLocks 2024-05-19 20:26:05 -05:00
82f0f177fb CE-938 add overload that takes a Duration 2024-05-19 20:24:55 -05:00
9c79ce3272 CE-938 add isPrimaryKey to @QField 2024-05-19 20:24:55 -05:00
85eae36c28 CE-938 Add concept of MetaDataProducerMultiOutput 2024-05-19 20:24:55 -05:00
485bc618e0 CE-938 update memoization to say if it should store null values or not 2024-05-19 20:21:03 -05:00
6f6f9af17d Updates for tests for min/max records 2024-05-17 16:40:00 -05:00
65fe6a002c Upgrade checkstyle; remove supressed indentation markers for new-style switches 2024-05-17 16:38:28 -05:00
ede497ee85 Add todos referencing lock-tree 2024-05-17 16:18:08 -05:00
5a56b5d9b4 Treat made-up primary keys as nulls... also, don't start them at -1 (which, idk, is maybe somewhat likely in some world? but instead of half of integer-min-value...) 2024-05-17 16:17:37 -05:00
e10a1e40da Implement min/max records enforcement 2024-05-17 16:16:45 -05:00
8816bc89c3 Add cases for merging an IN and IS_NOT_BLANK 2024-05-17 16:16:45 -05:00
759972b70c Fix chicken-egg session from repeating all-access key values 2024-05-17 16:16:45 -05:00
425629de52 Adding missing test 2024-05-17 16:16:45 -05:00
71ecde74df jdbc Connections in try-with-resources (so they close and return to connection pool) 2024-03-12 14:28:46 -05:00
7e34b97998 jdbc Connections in try-with-resources (so they close and return to connection pool) 2024-03-12 13:53:47 -05:00
1062f00ed4 Add c3p0 connection pooling to RDBMS module (ConnectionManager) 2024-03-12 12:02:36 -05:00
181 changed files with 9237 additions and 739 deletions

View 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

View 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

View 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
View 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" }}

View 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

View 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"

View 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
View 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
View 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
View 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
View 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
View File

@ -35,3 +35,5 @@ hs_err_pid*
*.swp
.flattened-pom.xml
dependency-reduced-pom.xml
/.env.local
/.cache/

View File

@ -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
View 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.

View File

@ -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)),
----

View File

@ -46,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>
@ -55,7 +55,7 @@
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
<coverage.haltOnFailure>true</coverage.haltOnFailure>
<coverage.instructionCoveredRatioMinimum>0.75</coverage.instructionCoveredRatioMinimum>
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
<plugin.shade.phase>none</plugin.shade.phase>
</properties>
@ -150,7 +150,7 @@
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>9.0</version>
<version>10.16.0</version>
</dependency>
</dependencies>
<executions>
@ -209,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
View 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

View File

@ -192,6 +192,13 @@
<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>

View File

@ -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));

View File

@ -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;

View File

@ -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);

View File

@ -127,7 +127,6 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public String getInsertOrUpdate()
{
return switch(this)

View File

@ -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) {}
/*******************************************************************************

View File

@ -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
/*******************************************************************************
**
*******************************************************************************/

View File

@ -256,7 +256,6 @@ public enum DateTimeGroupBy
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public Instant roundDown(Instant instant, ZoneId zoneId)
{
ZonedDateTime zoned = instant.atZone(zoneId);

View File

@ -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()));
}
}

View File

@ -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;

View File

@ -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;
};
}
}

View File

@ -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);
}
}

View File

@ -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."));
}
}
}
}
}

View File

@ -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. //
@ -335,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 //
///////////////////////////////////////////////////////////////////////////
@ -357,7 +364,7 @@ public class RunProcessAction
/*******************************************************************************
** Run a single backend step.
*******************************************************************************/
private RunBackendStepOutput 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());

View File

@ -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
**

View File

@ -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 //

View File

@ -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())));

View File

@ -77,7 +77,6 @@ public class ExecuteCodeAction
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException
{
QCodeReference codeReference = input.getCodeReference();

View File

@ -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)
{

View File

@ -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;

View File

@ -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();

View File

@ -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)

View File

@ -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());
}
/*******************************************************************************
**
*******************************************************************************/
@ -268,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)

View File

@ -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;
@ -335,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))
@ -395,7 +399,7 @@ public class UpdateAction
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, 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));

View File

@ -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();

View File

@ -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);
}

View File

@ -101,7 +101,7 @@ public class ValidateRecordSecurityLockHelper
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>());
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys);
/////////////////////////////////
// propagate errors to records //
@ -141,7 +141,7 @@ public class ValidateRecordSecurityLockHelper
** 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) throws QException
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)
{
@ -152,7 +152,7 @@ public class ValidateRecordSecurityLockHelper
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
treePosition.remove(treePosition.size() - 1);
i++;
}
@ -192,7 +192,7 @@ public class ValidateRecordSecurityLockHelper
}
Serializable recordSecurityValue = record.getValue(field.getName());
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
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);
@ -337,7 +337,7 @@ public class ValidateRecordSecurityLockHelper
for(QRecord inputRecord : inputRecords)
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
@ -370,14 +370,14 @@ public class ValidateRecordSecurityLockHelper
{
String primaryKeyField = table.getPrimaryKeyField();
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
Integer madeUpPrimaryKey = -1;
Integer madeUpPrimaryKey = Integer.MIN_VALUE / 2;
for(QRecord record : records)
{
if(record.getValue(primaryKeyField) == null)
{
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
record.setValue(primaryKeyField, madeUpPrimaryKey);
madeUpPrimaryKey--;
madeUpPrimaryKey++;
}
}
return madeUpPrimaryKeys;
@ -390,7 +390,6 @@ public class ValidateRecordSecurityLockHelper
** MultiRecordSecurityLock, with only the appropriate lock-scopes being included
** (e.g., read-locks for selects, write-locks for insert/update/delete).
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
static MultiRecordSecurityLock getRecordSecurityLocks(QTableMetaData table, Action action)
{
List<RecordSecurityLock> allLocksOnTable = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
@ -445,9 +444,9 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, 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 //

View File

@ -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<>();
@ -601,7 +561,7 @@ public class QPossibleValueTranslator
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(idField, QCriteriaOperator.IN, page)));
queryInput.setTransaction(getTransaction(tableName));
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 //

View File

@ -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);
}
}
}

View File

@ -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())

View File

@ -105,14 +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 stepName = nameValuePairs.get("step");
String widgetName = nameValuePairs.get("widget");
String slotName = nameValuePairs.get("slot");
@ -143,7 +150,7 @@ 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))
{
@ -153,6 +160,10 @@ public class QInstanceHelpContentManager
{
processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
}
else if(nameValuePairs.containsKey("instanceLevel"))
{
processHelpContentForInstance(key, slotName, roles, helpContent);
}
}
catch(Exception e)
{
@ -165,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)
@ -212,7 +223,21 @@ public class QInstanceHelpContentManager
}
else
{
LOG.info("Unrecognized key format for table help content", logPair("key", key));
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);
}
}
}
}
@ -307,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.

View File

@ -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
}

View File

@ -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))
{
@ -657,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());
@ -1468,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);
}
}
}
@ -1476,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 //
///////////////////////////////////////////////////////////////////////////////
@ -1487,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

View File

@ -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;

View File

@ -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());

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -49,8 +49,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
private ProcessState processState;
private Exception exception; // todo - make optional
private String overrideLastStepName;
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
private String overrideLastStepName; // todo - does this need to go into state too??
private List<AuditInput> auditInputList = new ArrayList<>();
@ -416,7 +415,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
return (this.processState.getUpdatedFrontendStepList());
}
@ -426,18 +425,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
*******************************************************************************/
public RunBackendStepOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
}

View File

@ -46,8 +46,6 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
private String processUUID;
private Optional<Exception> exception = Optional.empty();
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
/*******************************************************************************
@ -334,32 +332,21 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
/*******************************************************************************
** Getter for updatedFrontendStepList
*******************************************************************************/
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
return (this.updatedFrontendStepList);
}
/*******************************************************************************
** Setter for updatedFrontendStepList
**
*******************************************************************************/
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
{
this.updatedFrontendStepList = updatedFrontendStepList;
this.processState.setUpdatedFrontendStepList(updatedFrontendStepList);
}
/*******************************************************************************
** Fluent setter for updatedFrontendStepList
**
*******************************************************************************/
public RunProcessOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
{
this.updatedFrontendStepList = updatedFrontendStepList;
return (this);
return this.processState.getUpdatedFrontendStepList();
}
}

View File

@ -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
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -31,12 +31,16 @@ 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, Cloneable
{
@ -74,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
}
/*******************************************************************************
**
*******************************************************************************/
@ -683,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));
}
}

View File

@ -53,7 +53,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
}
else
{
storage = new QueryOutputList();
storage = new QueryOutputList(queryInput);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,196 @@
/*
* 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.model.dashboard.widgets;
import java.util.List;
/*******************************************************************************
** Model containing datastructure expected by frontend filter and columns setup widget
**
*******************************************************************************/
public class FilterAndColumnsSetupData extends QWidgetData
{
private String tableName;
private Boolean allowVariables = false;
private Boolean hideColumns = false;
private List<String> filterDefaultFieldNames;
/*******************************************************************************
**
*******************************************************************************/
public FilterAndColumnsSetupData()
{
}
/*******************************************************************************
**
*******************************************************************************/
public FilterAndColumnsSetupData(String tableName, Boolean allowVariables, Boolean hideColumns, List<String> filterDefaultFieldNames)
{
this.tableName = tableName;
this.allowVariables = allowVariables;
this.hideColumns = hideColumns;
this.filterDefaultFieldNames = filterDefaultFieldNames;
}
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public String getType()
{
return WidgetType.FILTER_AND_COLUMNS_SETUP.getType();
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public FilterAndColumnsSetupData withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for hideColumns
*******************************************************************************/
public Boolean getHideColumns()
{
return (this.hideColumns);
}
/*******************************************************************************
** Setter for hideColumns
*******************************************************************************/
public void setHideColumns(Boolean hideColumns)
{
this.hideColumns = hideColumns;
}
/*******************************************************************************
** Fluent setter for hideColumns
*******************************************************************************/
public FilterAndColumnsSetupData withHideColumns(Boolean hideColumns)
{
this.hideColumns = hideColumns;
return (this);
}
/*******************************************************************************
** Getter for filterDefaultFieldNames
*******************************************************************************/
public List<String> getFilterDefaultFieldNames()
{
return (this.filterDefaultFieldNames);
}
/*******************************************************************************
** Setter for filterDefaultFieldNames
*******************************************************************************/
public void setFilterDefaultFieldNames(List<String> filterDefaultFieldNames)
{
this.filterDefaultFieldNames = filterDefaultFieldNames;
}
/*******************************************************************************
** Fluent setter for filterDefaultFieldNames
*******************************************************************************/
public FilterAndColumnsSetupData withFilterDefaultFieldNames(List<String> filterDefaultFieldNames)
{
this.filterDefaultFieldNames = filterDefaultFieldNames;
return (this);
}
/*******************************************************************************
** Getter for allowVariables
*******************************************************************************/
public Boolean getAllowVariables()
{
return (this.allowVariables);
}
/*******************************************************************************
** Setter for allowVariables
*******************************************************************************/
public void setAllowVariables(Boolean allowVariables)
{
this.allowVariables = allowVariables;
}
/*******************************************************************************
** Fluent setter for allowVariables
*******************************************************************************/
public FilterAndColumnsSetupData withAllowVariables(Boolean allowVariables)
{
this.allowVariables = allowVariables;
return (this);
}
}

View File

@ -69,7 +69,7 @@ public enum WidgetType
DYNAMIC_FORM("dynamicForm"),
DATA_BAG_VIEWER("dataBagViewer"),
PIVOT_TABLE_SETUP("pivotTableSetup"),
REPORT_SETUP("reportSetup"),
FILTER_AND_COLUMNS_SETUP("filterAndColumnsSetup"),
SCRIPT_VIEWER("scriptViewer");

View File

@ -49,6 +49,11 @@ public @interface QField
*******************************************************************************/
String backendName() default "";
/*******************************************************************************
**
*******************************************************************************/
boolean isPrimaryKey() default false;
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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.data;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
** Marker - that a piece of code should be ignored (e.g., a field not treated as
** a @QField)
*******************************************************************************/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface QIgnore
{
}

View File

@ -35,12 +35,15 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang3.SerializationUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -462,6 +465,7 @@ public class QRecord implements Serializable
}
/*******************************************************************************
** Getter for a single field's value
**
@ -616,6 +620,22 @@ public class QRecord implements Serializable
/*******************************************************************************
** Getter for errors
**
*******************************************************************************/
@JsonIgnore
public String getErrorsAsString()
{
if(CollectionUtils.nullSafeHasContents(errors))
{
return StringUtils.join("; ", errors.stream().map(e -> e.getMessage()).toList());
}
return ("");
}
/*******************************************************************************
** Setter for errors
**
@ -732,6 +752,22 @@ public class QRecord implements Serializable
/*******************************************************************************
** Getter for warnings
**
*******************************************************************************/
@JsonIgnore
public String getWarningsAsString()
{
if(CollectionUtils.nullSafeHasContents(warnings))
{
return StringUtils.join("; ", warnings.stream().map(e -> e.getMessage()).toList());
}
return ("");
}
/*******************************************************************************
** Setter for warnings
*******************************************************************************/
@ -742,6 +778,18 @@ public class QRecord implements Serializable
/*******************************************************************************
** Fluently Add one warning to this record
**
*******************************************************************************/
public QRecord withWarning(QWarningMessage warning)
{
addWarning(warning);
return (this);
}
/*******************************************************************************
** Fluent setter for warnings
*******************************************************************************/

View File

@ -218,6 +218,7 @@ public abstract class QRecordEntity
}
/*******************************************************************************
**
*******************************************************************************/
@ -296,7 +297,19 @@ public abstract class QRecordEntity
}
else
{
LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
Optional<QIgnore> ignoreAnnotation = getQIgnoreAnnotation(c, fieldName);
Optional<QAssociation> associationAnnotation = getQAssociationAnnotation(c, fieldName);
if(ignoreAnnotation.isPresent() || associationAnnotation.isPresent())
{
////////////////////////////////////////////////////////////
// silently skip if marked as an association or an ignore //
////////////////////////////////////////////////////////////
}
else
{
LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
}
}
}
else
@ -360,6 +373,16 @@ public abstract class QRecordEntity
/*******************************************************************************
**
*******************************************************************************/
public static Optional<QIgnore> getQIgnoreAnnotation(Class<? extends QRecordEntity> c, String ignoreName)
{
return (getAnnotationOnField(c, QIgnore.class, ignoreName));
}
/*******************************************************************************
**
*******************************************************************************/
@ -419,9 +442,9 @@ public abstract class QRecordEntity
}
else
{
if(!method.getName().equals("getClass"))
if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null)
{
LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
}
}
}

View File

@ -145,7 +145,7 @@ public interface QRecordEnum
{
if(!method.getName().equals("getClass") && !method.getName().equals("getDeclaringClass") && !method.getName().equals("getPossibleValueId"))
{
LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
}
}
}

View File

@ -30,7 +30,7 @@ import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
** MetaDataProducerHelper, to put point at a package full of these, and populate
** your whole QInstance.
*******************************************************************************/
public abstract class MetaDataProducer<T extends TopLevelMetaDataInterface> implements MetaDataProducerInterface<T>
public abstract class MetaDataProducer<T extends MetaDataProducerOutput> implements MetaDataProducerInterface<T>
{
}

View File

@ -157,7 +157,7 @@ public class MetaDataProducerHelper
{
try
{
TopLevelMetaDataInterface metaData = producer.produce(instance);
MetaDataProducerOutput metaData = producer.produce(instance);
if(metaData != null)
{
metaData.addSelfToInstance(instance);

View File

@ -0,0 +1,101 @@
/*
* 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.metadata;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Output object for a MetaDataProducer, which contains multiple meta-data
** objects.
*******************************************************************************/
public class MetaDataProducerMultiOutput implements MetaDataProducerOutput
{
private List<MetaDataProducerOutput> contents;
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addSelfToInstance(QInstance instance)
{
for(MetaDataProducerOutput metaDataProducerOutput : CollectionUtils.nonNullList(contents))
{
metaDataProducerOutput.addSelfToInstance(instance);
}
}
/*******************************************************************************
**
*******************************************************************************/
public void add(MetaDataProducerOutput metaDataProducerOutput)
{
if(contents == null)
{
contents = new ArrayList<>();
}
contents.add(metaDataProducerOutput);
}
/*******************************************************************************
**
*******************************************************************************/
public MetaDataProducerMultiOutput with(MetaDataProducerOutput metaDataProducerOutput)
{
add(metaDataProducerOutput);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public <T extends MetaDataProducerOutput> List<T> getEach(Class<T> c)
{
List<T> rs = new ArrayList<>();
for(MetaDataProducerOutput content : contents)
{
if(content instanceof MetaDataProducerMultiOutput multiOutput)
{
rs.addAll(multiOutput.getEach(c));
}
else if(c.isInstance(content))
{
rs.add(c.cast(content));
}
}
return (rs);
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.metadata;
/*******************************************************************************
** Interface to mark objects that can be produced by a MetaDataProducer.
**
** These would usually be TopLevelMetaData objects (a table, a process, etc)
** but can also be a MetaDataProducerMultiOutput, to produce multiple objects
** from one producer.
*******************************************************************************/
public interface MetaDataProducerOutput
{
/*******************************************************************************
** call the appropriate methods on a QInstance to add ourselves to it.
*******************************************************************************/
void addSelfToInstance(QInstance instance);
}

View File

@ -33,7 +33,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationState;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
@ -44,6 +46,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNodeType;
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.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.messaging.QMessagingProviderMetaData;
@ -79,7 +83,7 @@ public class QInstance
private QAuthenticationMetaData authentication = null;
private QBrandingMetaData branding = null;
private Map<String, QAutomationProviderMetaData> automationProviders = new HashMap<>();
private Map<String, QMessagingProviderMetaData> messagingProviders = new HashMap<>();
private Map<String, QMessagingProviderMetaData> messagingProviders = new HashMap<>();
////////////////////////////////////////////////////////////////////////////////////////////
// Important to use LinkedHashmap here, to preserve the order in which entries are added. //
@ -100,6 +104,8 @@ public class QInstance
private Map<String, QSupplementalInstanceMetaData> supplementalMetaData = new LinkedHashMap<>();
protected Map<String, List<QHelpContent>> helpContent;
private String deploymentMode;
private Map<String, String> environmentValues = new LinkedHashMap<>();
private String defaultTimeZoneId = "UTC";
@ -107,10 +113,13 @@ public class QInstance
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
// todo - lock down the object (no more changes allowed) after it's been validated?
//////////////////////////////////////////////////////////////////////////////////////
// todo - lock down the object (no more changes allowed) after it's been validated? //
// if doing so, may need to copy all of the collections into read-only versions... //
//////////////////////////////////////////////////////////////////////////////////////
@JsonIgnore
private boolean hasBeenValidated = false;
private QInstanceValidationState validationState = QInstanceValidationState.PENDING;
private Map<String, String> memoizedTablePaths = new HashMap<>();
private Map<String, String> memoizedProcessPaths = new HashMap<>();
@ -794,32 +803,58 @@ public class QInstance
*******************************************************************************/
public boolean getHasBeenValidated()
{
return hasBeenValidated;
return validationState.equals(QInstanceValidationState.COMPLETE);
}
/*******************************************************************************
** If pass a QInstanceValidationKey (which can only be instantiated by the validator),
** then the hasBeenValidated field will be set to true.
** then the validationState will be set to COMPLETE.
**
** Else, if passed a null, hasBeenValidated will be reset to false - e.g., to
** Else, if passed a null, the validationState will be reset to PENDING. e.g., to
** re-trigger validation (can be useful in tests).
*******************************************************************************/
public void setHasBeenValidated(QInstanceValidationKey key)
{
if(key == null)
{
this.hasBeenValidated = false;
this.validationState = QInstanceValidationState.PENDING;
}
else
{
this.hasBeenValidated = true;
this.validationState = QInstanceValidationState.COMPLETE;
}
}
/*******************************************************************************
** If pass a QInstanceValidationKey (which can only be instantiated by the validator),
** then the validationState set to RUNNING.
**
*******************************************************************************/
public void setValidationIsRunning(QInstanceValidationKey key)
{
if(key != null)
{
this.validationState = QInstanceValidationState.RUNNING;
}
}
/*******************************************************************************
** check if the instance is currently running validation.
**
*******************************************************************************/
public boolean getValidationIsRunning()
{
return validationState.equals(QInstanceValidationState.RUNNING);
}
/*******************************************************************************
** Getter for branding
**
@ -1380,4 +1415,74 @@ public class QInstance
this.schedulableTypes = schedulableTypes;
}
/*******************************************************************************
** Getter for helpContent
*******************************************************************************/
public Map<String, List<QHelpContent>> getHelpContent()
{
return (this.helpContent);
}
/*******************************************************************************
** Setter for helpContent
*******************************************************************************/
public void setHelpContent(Map<String, List<QHelpContent>> helpContent)
{
this.helpContent = helpContent;
}
/*******************************************************************************
** Fluent setter for helpContent
*******************************************************************************/
public QInstance withHelpContent(Map<String, List<QHelpContent>> helpContent)
{
this.helpContent = helpContent;
return (this);
}
/*******************************************************************************
** Fluent setter for adding 1 helpContent (for a slot)
*******************************************************************************/
public QInstance withHelpContent(String slot, QHelpContent helpContent)
{
if(this.helpContent == null)
{
this.helpContent = new HashMap<>();
}
List<QHelpContent> listForSlot = this.helpContent.computeIfAbsent(slot, (k) -> new ArrayList<>());
QInstanceHelpContentManager.putHelpContentInList(helpContent, listForSlot);
return (this);
}
/*******************************************************************************
** remove a helpContent for a slot based on its set of roles
*******************************************************************************/
public void removeHelpContent(String slot, Set<HelpRole> roles)
{
if(this.helpContent == null)
{
return;
}
List<QHelpContent> listForSlot = this.helpContent.get(slot);
if(listForSlot == null)
{
return;
}
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
}
}

View File

@ -26,7 +26,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
** Interface for meta-data classes that can be added directly (e.g, at the top
** level) to a QInstance (such as a QTableMetaData - not a QFieldMetaData).
*******************************************************************************/
public interface TopLevelMetaDataInterface
public interface TopLevelMetaDataInterface extends MetaDataProducerOutput
{
/*******************************************************************************

View File

@ -0,0 +1,172 @@
/*
* 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.metadata.fields;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Field behavior that changes the case of string values.
*******************************************************************************/
public enum CaseChangeBehavior implements FieldBehavior<CaseChangeBehavior>, FieldBehaviorForFrontend, FieldFilterBehavior<CaseChangeBehavior>
{
NONE(null),
TO_UPPER_CASE((String s) -> s.toUpperCase()),
TO_LOWER_CASE((String s) -> s.toLowerCase());
private final Function<String, String> function;
/*******************************************************************************
**
*******************************************************************************/
CaseChangeBehavior(Function<String, String> function)
{
this.function = function;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public CaseChangeBehavior getDefault()
{
return (NONE);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
if(this.equals(NONE))
{
return;
}
switch(this)
{
case TO_UPPER_CASE, TO_LOWER_CASE -> applyFunction(recordList, table, field);
default -> throw new IllegalStateException("Unexpected enum value: " + this);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void applyFunction(List<QRecord> recordList, QTableMetaData table, QFieldMetaData field)
{
String fieldName = field.getName();
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
String value = record.getValueString(fieldName);
if(value != null && function != null)
{
record.setValue(fieldName, function.apply(value));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable applyToFilterCriteriaValue(Serializable value, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
if(this.equals(NONE) || function == null)
{
return (value);
}
if(value instanceof String s)
{
String newValue = function.apply(s);
if(!Objects.equals(value, newValue))
{
return (newValue);
}
}
return (value);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean allowMultipleBehaviorsOfThisType()
{
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<String> validateBehaviorConfiguration(QTableMetaData tableMetaData, QFieldMetaData fieldMetaData)
{
if(this == NONE)
{
return Collections.emptyList();
}
List<String> errors = new ArrayList<>();
String errorSuffix = " field [" + fieldMetaData.getName() + "] in table [" + tableMetaData.getName() + "]";
if(fieldMetaData.getType() != null)
{
if(!fieldMetaData.getType().isStringLike())
{
errors.add("A CaseChange was a applied to a non-String-like field:" + errorSuffix);
}
}
return (errors);
}
}

View File

@ -49,7 +49,6 @@ public interface DisplayFormat
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:Indentation")
static String getExcelFormat(String javaDisplayFormat)
{
if(javaDisplayFormat == null)
@ -58,21 +57,21 @@ public interface DisplayFormat
}
return switch(javaDisplayFormat)
{
case DisplayFormat.DEFAULT -> null;
case DisplayFormat.COMMAS -> "#,##0";
case DisplayFormat.DECIMAL1 -> "0.0";
case DisplayFormat.DECIMAL2 -> "0.00";
case DisplayFormat.DECIMAL3 -> "0.000";
case DisplayFormat.DECIMAL1_COMMAS -> "#,##0.0";
case DisplayFormat.DECIMAL2_COMMAS -> "#,##0.00";
case DisplayFormat.DECIMAL3_COMMAS -> "#,##0.000";
case DisplayFormat.CURRENCY -> "$#,##0.00";
case DisplayFormat.PERCENT -> "0%";
case DisplayFormat.PERCENT_POINT1 -> "0.0%";
case DisplayFormat.PERCENT_POINT2 -> "0.00%";
default -> null;
};
{
case DisplayFormat.DEFAULT -> null;
case DisplayFormat.COMMAS -> "#,##0";
case DisplayFormat.DECIMAL1 -> "0.0";
case DisplayFormat.DECIMAL2 -> "0.00";
case DisplayFormat.DECIMAL3 -> "0.000";
case DisplayFormat.DECIMAL1_COMMAS -> "#,##0.0";
case DisplayFormat.DECIMAL2_COMMAS -> "#,##0.00";
case DisplayFormat.DECIMAL3_COMMAS -> "#,##0.000";
case DisplayFormat.CURRENCY -> "$#,##0.00";
case DisplayFormat.PERCENT -> "0%";
case DisplayFormat.PERCENT_POINT1 -> "0.0%";
case DisplayFormat.PERCENT_POINT2 -> "0.00%";
default -> null;
};
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.metadata.fields;
import java.io.Serializable;
/*******************************************************************************
** Marker interface for a field behavior which you might want to send to a
** frontend (e.g., so it can edit values to match what'll happen in the backend).
*******************************************************************************/
public interface FieldBehaviorForFrontend extends Serializable
{
}

View File

@ -23,7 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
/*******************************************************************************
**
** Interface to mark a field behavior as one to be used during generating
** display values.
*******************************************************************************/
public interface FieldDisplayBehavior<T extends FieldDisplayBehavior<T>> extends FieldBehavior<T>
{

View File

@ -0,0 +1,43 @@
/*
* 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.metadata.fields;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Interface to mark a field behavior as one to be used before a query filter
** is executed.
*******************************************************************************/
public interface FieldFilterBehavior<T extends FieldFilterBehavior<T>> extends FieldBehavior<T>
{
/*******************************************************************************
** Apply the filter to a value from a criteria.
** If you don't want to change the input value, return the parameter.
*******************************************************************************/
Serializable applyToFilterCriteriaValue(Serializable value, QInstance instance, QTableMetaData table, QFieldMetaData field);
}

View File

@ -23,13 +23,17 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehaviorForFrontend;
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.help.QHelpContent;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@ -53,6 +57,8 @@ public class QFrontendFieldMetaData
private List<FieldAdornment> adornments;
private List<QHelpContent> helpContents;
private List<FieldBehaviorForFrontend> behaviors;
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
//////////////////////////////////////////////////////////////////////////////////
@ -75,6 +81,18 @@ public class QFrontendFieldMetaData
this.adornments = fieldMetaData.getAdornments();
this.defaultValue = fieldMetaData.getDefaultValue();
this.helpContents = fieldMetaData.getHelpContents();
for(FieldBehavior<?> behavior : CollectionUtils.nonNullCollection(fieldMetaData.getBehaviors()))
{
if(behavior instanceof FieldBehaviorForFrontend fbff)
{
if(behaviors == null)
{
behaviors = new ArrayList<>();
}
behaviors.add(fbff);
}
}
}
@ -198,4 +216,14 @@ public class QFrontendFieldMetaData
return helpContents;
}
/*******************************************************************************
** Getter for fieldBehaviors
**
*******************************************************************************/
public List<FieldBehaviorForFrontend> getBehaviors()
{
return behaviors;
}
}

View File

@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
@ -76,7 +77,8 @@ public class QFrontendTableMetaData
private boolean usesVariants;
private String variantTableLabel;
private ShareableTableMetaData shareableTableMetaData;
private ShareableTableMetaData shareableTableMetaData;
private Map<String, List<QHelpContent>> helpContents;
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
@ -172,6 +174,8 @@ public class QFrontendTableMetaData
usesVariants = true;
variantTableLabel = actionInput.getInstance().getTable(backend.getVariantOptionsTableName()).getLabel();
}
this.helpContents = tableMetaData.getHelpContent();
}
@ -382,4 +386,15 @@ public class QFrontendTableMetaData
{
return shareableTableMetaData;
}
/*******************************************************************************
** Getter for helpContents
**
*******************************************************************************/
public Map<String, List<QHelpContent>> getHelpContents()
{
return helpContents;
}
}

View File

@ -44,14 +44,13 @@ public enum JoinType
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public JoinType flip()
{
return switch(this)
{
case ONE_TO_MANY -> MANY_TO_ONE;
case MANY_TO_ONE -> ONE_TO_MANY;
case MANY_TO_MANY, ONE_TO_ONE -> this;
};
{
case ONE_TO_MANY -> MANY_TO_ONE;
case MANY_TO_ONE -> ONE_TO_MANY;
case MANY_TO_MANY, ONE_TO_ONE -> this;
};
}
}

View File

@ -60,6 +60,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map<String, QStepMetaData> steps; // this is the full map of possible steps
private QBackendStepMetaData cancelStep;
private QIcon icon;
private QScheduleMetaData schedule;
@ -675,6 +677,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
}
/*******************************************************************************
** Getter for variantRunStrategy
*******************************************************************************/
@ -746,4 +749,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return steps;
}
/*******************************************************************************
** Getter for cancelStep
*******************************************************************************/
public QBackendStepMetaData getCancelStep()
{
return (this.cancelStep);
}
/*******************************************************************************
** Setter for cancelStep
*******************************************************************************/
public void setCancelStep(QBackendStepMetaData cancelStep)
{
this.cancelStep = cancelStep;
}
/*******************************************************************************
** Fluent setter for cancelStep
*******************************************************************************/
public QProcessMetaData withCancelStep(QBackendStepMetaData cancelStep)
{
this.cancelStep = cancelStep;
return (this);
}
}

View File

@ -0,0 +1,128 @@
/*
* 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.metadata.queues;
/*******************************************************************************
** settings that can be applied to either an SQSQueue or an SQSQueueProvider,
** to control what the SQSQueuePoller does when it receives from AWS.
*******************************************************************************/
public class SQSPollerSettings
{
private Integer maxNumberOfMessages;
private Integer waitTimeSeconds;
private Integer maxLoops;
/*******************************************************************************
** Getter for maxNumberOfMessages
*******************************************************************************/
public Integer getMaxNumberOfMessages()
{
return (this.maxNumberOfMessages);
}
/*******************************************************************************
** Setter for maxNumberOfMessages
*******************************************************************************/
public void setMaxNumberOfMessages(Integer maxNumberOfMessages)
{
this.maxNumberOfMessages = maxNumberOfMessages;
}
/*******************************************************************************
** Fluent setter for maxNumberOfMessages
*******************************************************************************/
public SQSPollerSettings withMaxNumberOfMessages(Integer maxNumberOfMessages)
{
this.maxNumberOfMessages = maxNumberOfMessages;
return (this);
}
/*******************************************************************************
** Getter for waitTimeSeconds
*******************************************************************************/
public Integer getWaitTimeSeconds()
{
return (this.waitTimeSeconds);
}
/*******************************************************************************
** Setter for waitTimeSeconds
*******************************************************************************/
public void setWaitTimeSeconds(Integer waitTimeSeconds)
{
this.waitTimeSeconds = waitTimeSeconds;
}
/*******************************************************************************
** Fluent setter for waitTimeSeconds
*******************************************************************************/
public SQSPollerSettings withWaitTimeSeconds(Integer waitTimeSeconds)
{
this.waitTimeSeconds = waitTimeSeconds;
return (this);
}
/*******************************************************************************
** Getter for maxLoops
*******************************************************************************/
public Integer getMaxLoops()
{
return (this.maxLoops);
}
/*******************************************************************************
** Setter for maxLoops
*******************************************************************************/
public void setMaxLoops(Integer maxLoops)
{
this.maxLoops = maxLoops;
}
/*******************************************************************************
** Fluent setter for maxLoops
*******************************************************************************/
public SQSPollerSettings withMaxLoops(Integer maxLoops)
{
this.maxLoops = maxLoops;
return (this);
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.metadata.queues;
/*******************************************************************************
** SQS subclass of meta-data for a specific Queue
*******************************************************************************/
public class SQSQueueMetaData extends QQueueMetaData
{
private SQSPollerSettings pollerSettings;
/*******************************************************************************
** Getter for pollerSettings
*******************************************************************************/
public SQSPollerSettings getPollerSettings()
{
return (this.pollerSettings);
}
/*******************************************************************************
** Setter for pollerSettings
*******************************************************************************/
public void setPollerSettings(SQSPollerSettings pollerSettings)
{
this.pollerSettings = pollerSettings;
}
/*******************************************************************************
** Fluent setter for pollerSettings
*******************************************************************************/
public SQSQueueMetaData withPollerSettings(SQSPollerSettings pollerSettings)
{
this.pollerSettings = pollerSettings;
return (this);
}
}

View File

@ -36,6 +36,8 @@ public class SQSQueueProviderMetaData extends QQueueProviderMetaData
private String region;
private String baseURL;
private SQSPollerSettings pollerSettings;
/*******************************************************************************
@ -196,4 +198,35 @@ public class SQSQueueProviderMetaData extends QQueueProviderMetaData
return (this);
}
/*******************************************************************************
** Getter for pollerSettings
*******************************************************************************/
public SQSPollerSettings getPollerSettings()
{
return (this.pollerSettings);
}
/*******************************************************************************
** Setter for pollerSettings
*******************************************************************************/
public void setPollerSettings(SQSPollerSettings pollerSettings)
{
this.pollerSettings = pollerSettings;
}
/*******************************************************************************
** Fluent setter for pollerSettings
*******************************************************************************/
public SQSQueueProviderMetaData withPollerSettings(SQSPollerSettings pollerSettings)
{
this.pollerSettings = pollerSettings;
return (this);
}
}

View File

@ -39,18 +39,21 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
*******************************************************************************/
public class QStepMetaDataDeserializer extends JsonDeserializer<QStepMetaData>
{
/***************************************************************************
**
***************************************************************************/
@Override
@SuppressWarnings("checkstyle:Indentation")
public QStepMetaData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException
{
TreeNode treeNode = jsonParser.readValueAsTree();
String stepType = DeserializerUtils.readTextValue(treeNode, "stepType");
Class<? extends QStepMetaData> targetClass = switch(stepType)
{
case "backend" -> QBackendStepMetaData.class;
case "frontend" -> QFrontendStepMetaData.class;
default -> throw new IllegalArgumentException("Unsupported StepType " + stepType + " for deserialization");
};
{
case "backend" -> QBackendStepMetaData.class;
case "frontend" -> QFrontendStepMetaData.class;
default -> throw new IllegalArgumentException("Unsupported StepType " + stepType + " for deserialization");
};
return (DeserializerUtils.reflectivelyDeserialize(targetClass, treeNode));
}

View File

@ -34,6 +34,7 @@ import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField;
@ -43,6 +44,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
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.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
@ -110,6 +113,9 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private ShareableTableMetaData shareableTableMetaData;
protected Map<String, List<QHelpContent>> helpContent;
/*******************************************************************************
** Default constructor.
@ -158,11 +164,26 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
public QTableMetaData withFieldsFromEntity(Class<? extends QRecordEntity> entityClass) throws QException
{
List<QRecordEntityField> recordEntityFieldList = QRecordEntity.getFieldList(entityClass);
boolean setPrimaryKey = false;
for(QRecordEntityField recordEntityField : recordEntityFieldList)
{
QFieldMetaData field = new QFieldMetaData(recordEntityField.getGetter());
addField(field);
if(recordEntityField.getFieldAnnotation().isPrimaryKey())
{
if(setPrimaryKey)
{
throw (new QException("Attempt to set more than one field as primary key (" + primaryKeyField + "," + field.getName() + ")."));
}
setPrimaryKeyField(field.getName());
setPrimaryKey = true;
}
}
return (this);
}
@ -624,6 +645,18 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
/*******************************************************************************
** fluent setter for both recordLabelFormat and recordLabelFields
*******************************************************************************/
public QTableMetaData withRecordLabelFormatAndFields(String format, String... fields)
{
setRecordLabelFormat(format);
setRecordLabelFields(Arrays.asList(fields));
return (this);
}
/*******************************************************************************
** Getter for recordLabelFields
**
@ -1388,6 +1421,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
}
/*******************************************************************************
** Getter for shareableTableMetaData
*******************************************************************************/
@ -1418,4 +1452,73 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
}
/*******************************************************************************
** Getter for helpContent
*******************************************************************************/
public Map<String, List<QHelpContent>> getHelpContent()
{
return (this.helpContent);
}
/*******************************************************************************
** Setter for helpContent
*******************************************************************************/
public void setHelpContent(Map<String, List<QHelpContent>> helpContent)
{
this.helpContent = helpContent;
}
/*******************************************************************************
** Fluent setter for helpContent
*******************************************************************************/
public QTableMetaData withHelpContent(Map<String, List<QHelpContent>> helpContent)
{
this.helpContent = helpContent;
return (this);
}
/*******************************************************************************
** Fluent setter for adding 1 helpContent (for a slot)
*******************************************************************************/
public QTableMetaData withHelpContent(String slot, QHelpContent helpContent)
{
if(this.helpContent == null)
{
this.helpContent = new HashMap<>();
}
List<QHelpContent> listForSlot = this.helpContent.computeIfAbsent(slot, (k) -> new ArrayList<>());
QInstanceHelpContentManager.putHelpContentInList(helpContent, listForSlot);
return (this);
}
/*******************************************************************************
** remove a helpContent for a slot based on its set of roles
*******************************************************************************/
public void removeHelpContent(String slot, Set<HelpRole> roles)
{
if(this.helpContent == null)
{
return;
}
List<QHelpContent> listForSlot = this.helpContent.get(slot);
if(listForSlot == null)
{
return;
}
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
}
}

View File

@ -28,6 +28,7 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
@ -77,7 +78,9 @@ public class QueryStat extends QRecordEntity
///////////////////////////////////////////////////////////
// non-persistent fields - used to help build the record //
///////////////////////////////////////////////////////////
private String tableName;
@QIgnore
private String tableName;
private Set<String> joinTableNames;
private QQueryFilter queryFilter;
@ -384,6 +387,7 @@ public class QueryStat extends QRecordEntity
/*******************************************************************************
** Getter for queryFilter
*******************************************************************************/
@QIgnore
public QQueryFilter getQueryFilter()
{
return (this.queryFilter);
@ -446,6 +450,7 @@ public class QueryStat extends QRecordEntity
/*******************************************************************************
** Getter for joinTableNames
*******************************************************************************/
@QIgnore
public Set<String> getJoinTableNames()
{
return (this.joinTableNames);

View File

@ -0,0 +1,45 @@
/*
* 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.savedreports;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.FilterAndColumnsSetupData;
/*******************************************************************************
**
*******************************************************************************/
public class SavedReportsFilterAndColumnsSetupRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return (new RenderWidgetOutput(new FilterAndColumnsSetupData(null, true, false, null)));
}
}

View File

@ -73,6 +73,7 @@ public class SavedReportsMetaDataProvider
public static final String RENDER_REPORT_PROCESS_VALUES_WIDGET = "renderReportProcessValuesWidget";
/*******************************************************************************
**
*******************************************************************************/
@ -234,8 +235,8 @@ public class SavedReportsMetaDataProvider
.withName("reportSetupWidget")
.withLabel("Filters and Columns")
.withIsCard(true)
.withType(WidgetType.REPORT_SETUP.getType())
.withCodeReference(new QCodeReference(DefaultWidgetRenderer.class));
.withType(WidgetType.FILTER_AND_COLUMNS_SETUP.getType())
.withCodeReference(new QCodeReference(SavedReportsFilterAndColumnsSetupRenderer.class));
}

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QIgnore;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
@ -434,6 +435,7 @@ public class ScheduledJob extends QRecordEntity
/*******************************************************************************
** Getter for jobParameters - but a map of just the key=value pairs.
*******************************************************************************/
@QIgnore
public Map<String, String> getJobParametersMap()
{
if(CollectionUtils.nullSafeIsEmpty(this.jobParameters))
@ -469,6 +471,7 @@ public class ScheduledJob extends QRecordEntity
}
/*******************************************************************************
** Getter for repeatSeconds
*******************************************************************************/

View File

@ -0,0 +1,125 @@
/*
* 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.session;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Special session, indicating that an action being executed is being done not
** on behalf of a (human or otherwise) user - but instead, is the application/
** system itself.
**
** Generally this means, escalated privileges - e.g., permission to all tables,
** processes etc, and all security keys (e.g., all-access keys).
*******************************************************************************/
public class QSystemUserSession extends QSession
{
private static final QLogger LOG = QLogger.getLogger(QSystemUserSession.class);
private static List<String> allAccessKeyNames = null;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QSystemUserSession()
{
super();
////////////////////////////////////////////////////////
// always give system user all of the all-access keys //
////////////////////////////////////////////////////////
for(String allAccessKeyName : getAllAccessKeyNames())
{
withSecurityKeyValue(allAccessKeyName, true);
}
}
/*******************************************************************************
** System User Sessions should always have permission to all the things.
*******************************************************************************/
@Override
public boolean hasPermission(String permissionName)
{
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
private List<String> getAllAccessKeyNames()
{
if(allAccessKeyNames == null)
{
QInstance qInstance = QContext.getQInstance();
if(qInstance == null)
{
LOG.warn("QInstance was not set in context when creating a QSystemUserSession and trying to prime allAccessKeyNames... This SystemUserSession will NOT have any allAccessKeys.");
return (Collections.emptyList());
}
///////////////////////////////////////////////////////////////////////////////////////
// ideally only 1 thread would do this, but, it's cheap, so don't bother locking. //
// and, if multiple get in, only the last one will assign to the field, so, s/b fine //
///////////////////////////////////////////////////////////////////////////////////////
List<String> list = new ArrayList<>();
for(QSecurityKeyType securityKeyType : qInstance.getSecurityKeyTypes().values())
{
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
list.add(securityKeyType.getAllAccessKeyName());
}
}
LOG.info("Initialized allAccessKeyNames for SystemUserSessions as: " + list);
allAccessKeyNames = list;
}
return (allAccessKeyNames);
}
/*******************************************************************************
** Meant for use in tests - to explicitly null-out the allAccessKeyNames field.
*******************************************************************************/
static void unsetAllAccessKeyNames()
{
allAccessKeyNames = null;
}
}

View File

@ -72,6 +72,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
@ -150,10 +151,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is how we allow the actions within this class to work without themselves having a logged-in user. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
private static QSession chickenAndEggSession = new QSession()
{
};
private static QSession chickenAndEggSession = null;
@ -163,14 +161,29 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
*******************************************************************************/
private QSession getChickenAndEggSession()
{
for(String typeName : QContext.getQInstance().getSecurityKeyTypes().keySet())
if(chickenAndEggSession == null)
{
QSecurityKeyType keyType = QContext.getQInstance().getSecurityKeyType(typeName);
if(StringUtils.hasContent(keyType.getAllAccessKeyName()))
////////////////////////////////////////////////////////////////////////////////
// if the static field is null, then let's make a new session; //
// prime it with all all-access keys; and then set it in the static field. //
// and, if 2 threads get in here at the same time, no real harm will be done, //
// other than creating the session twice, and whoever loses the race, that'll //
// be the one that stays in the field //
////////////////////////////////////////////////////////////////////////////////
QSession newChickenAndEggSession = new QSession();
for(String typeName : QContext.getQInstance().getSecurityKeyTypes().keySet())
{
chickenAndEggSession = chickenAndEggSession.withSecurityKeyValue(keyType.getAllAccessKeyName(), true);
QSecurityKeyType keyType = QContext.getQInstance().getSecurityKeyType(typeName);
if(StringUtils.hasContent(keyType.getAllAccessKeyName()))
{
newChickenAndEggSession.withSecurityKeyValue(keyType.getAllAccessKeyName(), true);
}
}
chickenAndEggSession = newChickenAndEggSession;
}
return (chickenAndEggSession);
}
@ -196,7 +209,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// process a sessionUUID - looks up userSession record - cannot create token this way. //
/////////////////////////////////////////////////////////////////////////////////////////
String sessionUUID = context.get(SESSION_UUID_KEY);
LOG.debug("Creating session from sessionUUID (userSession)", logPair("sessionUUID", maskForLog(sessionUUID)));
LOG.trace("Creating session from sessionUUID (userSession)", logPair("sessionUUID", maskForLog(sessionUUID)));
if(sessionUUID != null)
{
accessToken = getAccessTokenFromSessionUUID(metaData, sessionUUID);
@ -254,7 +267,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// decode the credentials from the header auth //
/////////////////////////////////////////////////
String base64Credentials = context.get(BASIC_AUTH_KEY).trim();
LOG.info("Creating session from basicAuthentication", logPair("base64Credentials", maskForLog(base64Credentials)));
LOG.trace("Creating session from basicAuthentication", logPair("base64Credentials", maskForLog(base64Credentials)));
accessToken = getAccessTokenFromBase64BasicAuthCredentials(metaData, auth, base64Credentials);
}
catch(Auth0Exception e)
@ -273,7 +286,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// process an api key - looks up client application token (creating token if needed) //
///////////////////////////////////////////////////////////////////////////////////////
String apiKey = context.get(API_KEY);
LOG.info("Creating session from apiKey (accessTokenTable)", logPair("apiKey", maskForLog(apiKey)));
LOG.trace("Creating session from apiKey (accessTokenTable)", logPair("apiKey", maskForLog(apiKey)));
if(apiKey != null)
{
accessToken = getAccessTokenFromApiKey(metaData, apiKey);
@ -479,6 +492,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
return (true);
}
if(session instanceof QSystemUserSession)
{
return (true);
}
if(session == null)
{
return (false);

View File

@ -51,6 +51,7 @@ 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.authentication.TableBasedAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
@ -256,6 +257,11 @@ public class TableBasedAuthenticationModule implements QAuthenticationModuleInte
return (true);
}
if(session instanceof QSystemUserSession)
{
return (true);
}
if(session == null)
{
return (false);

View File

@ -769,7 +769,6 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
private static Serializable computeAggregate(List<QRecord> records, Aggregate aggregate, QTableMetaData table)
{
String fieldName = aggregate.getFieldName();

View File

@ -38,7 +38,6 @@ public class MockCountAction implements CountInterface
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:MagicNumber")
public CountOutput execute(CountInput countInput) throws QException
{
try

View File

@ -95,10 +95,8 @@ public class MockQueryAction implements QueryInterface
** Get a mock value to use, based on its type.
**
*******************************************************************************/
@SuppressWarnings("checkstyle:MagicNumber")
public static Serializable getMockValue(QTableMetaData table, String field)
{
// @formatter:off // IJ can't do new-style switch correctly yet...
return switch(table.getField(field).getType())
{
case STRING -> UUID.randomUUID().toString();
@ -112,7 +110,6 @@ public class MockQueryAction implements QueryInterface
case PASSWORD -> "abc***234";
default -> throw new IllegalStateException("Unexpected value: " + table.getField(field).getType());
};
// @formatter:on
}
}

Some files were not shown because too many files have changed in this diff Show More