Compare commits

..

23 Commits

Author SHA1 Message Date
9c60c483aa Checkpoint Commit
- Working Unified (qqq+kof22website) project setup and configuration working with IntelliJ 2025
2025-07-17 18:50:49 -05:00
753f8bb276 Reverted typo 2025-07-08 16:53:56 -05:00
a131bc782e Checkpoint Commit - Working larger test data and frontend polishing 2025-07-02 09:46:39 -05:00
6262974a4c Added full support for defining SPAs within the metadata for a route provider (both spa hosted path, and physical file path) 2025-06-24 16:59:35 -05:00
fe0c9f4b9c Test commit - sync between machines 2025-06-22 08:11:08 -05:00
b93f262622 Initial refactoring to abstract baseclass to allow for additional functionality across all routers 2025-06-17 15:16:57 -05:00
a6047dcc18 Initial refactoring to abstract baseclass to allow for additional functionality across all routers 2025-06-17 15:16:50 -05:00
fecfb5c19a Updated test files for positive and negative tests cases 2025-06-17 15:15:41 -05:00
fca857cf98 Updated test files for positive and negative tests cases 2025-06-17 15:15:14 -05:00
e558450f6b Updated checkstyle base version 2025-06-17 15:14:40 -05:00
d3ce24d00e Ignore local IntelliJ config files 2025-06-17 14:54:50 -05:00
7575a57ae9 Updated Property Var name per review 2025-06-17 13:05:14 -05:00
54f40fbc83 Added exception to warning message per review 2025-06-17 13:02:28 -05:00
caa6723cd2 Updated comments on Getters and Settings to see the flutter method for details. 2025-06-17 13:00:34 -05:00
b21ea60c80 Updated static var references (to class from instance) 2025-06-17 09:45:25 -05:00
ea15640db1 Cleaned up logging and converted to LogPairs per DK feedback 2025-06-17 09:42:12 -05:00
9cb401a20e Added support (and tests) for overriding the default hosted path for the material-frontend-ui 2025-06-17 09:41:45 -05:00
010b64a0d3 Added for valid local tests of loading the front-end UI from different hosted paths 2025-06-17 09:41:07 -05:00
46bca6efb9 Merge pull request #184 from Kingsrook/183-javalin-server-fails-to-start-when-using-static-files-in-a-production-jar
Fixed loading static files from FS or Jars
2025-06-15 11:04:52 -05:00
f6859d040f Refactored to use the constructor instead of the class/static method to load properties - makes unit test runtime cleaning 2025-06-15 10:36:11 -05:00
d13fc4a863 Removed - Merged back into overall unit tests 2025-06-15 10:35:18 -05:00
eab87b9d80 Added missing jar for unit test 2025-06-15 10:01:11 -05:00
707400a8b2 Added support for loading static files from the filesystem as as from jars (based on a system property) 2025-06-14 16:07:51 -05:00
84 changed files with 595 additions and 5015 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA" serialisationVersion="2">
<checkstyleVersion>9.0.1</checkstyleVersion>
<checkstyleVersion>10.3.4</checkstyleVersion>
<scanScope>JavaOnly</scanScope>
<suppressErrors>true</suppressErrors>
<option name="thirdPartyClasspath" />

29
.idea/compiler.xml generated
View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="qqq-backend-module-rdbms" />
<module name="qqq-middleware-picocli" />
<module name="qqq-backend-module-filesystem" />
<module name="qqq-backend-core" />
<module name="qqq-middleware-javalin" />
<module name="qqq-sample-project" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="qqq-backend-core" options="-Xlint:unchecked" />
<module name="qqq-backend-module-filesystem" options="-Xlint:unchecked" />
<module name="qqq-backend-module-rdbms" options="-Xlint:unchecked" />
<module name="qqq-middleware-javalin" options="-Xlint:unchecked" />
<module name="qqq-middleware-picocli" options="-Xlint:unchecked" />
<module name="qqq-parent-project" options="" />
<module name="qqq-sample-project" options="-Xlint:unchecked" />
</option>
</component>
</project>

19
.idea/encodings.xml generated
View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/qqq-backend-core/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-backend-core/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-backend-module-filesystem/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-backend-module-filesystem/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-backend-module-rdbms/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-backend-module-rdbms/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-middleware-javalin/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-middleware-javalin/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-middleware-picocli/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-middleware-picocli/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-sample-project/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/qqq-sample-project/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@ -1,226 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.customizers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
/*******************************************************************************
** Implementation of TableCustomizerInterface that runs multiple other customizers
*******************************************************************************/
public class MultiCustomizer implements InitializableViaCodeReference, TableCustomizerInterface
{
private static final String KEY_CODE_REFERENCES = "codeReferences";
private List<TableCustomizerInterface> customizers = new ArrayList<>();
/***************************************************************************
* Factory method that builds a {@link QCodeReferenceWithProperties} that will
* allow this multi-customizer to be assigned to a table, and to track
* in that code ref's properties, the "sub" QCodeReferences to be used.
*
* Added to a table as in:
* <pre>
* table.withCustomizer(TableCustomizers.POST_INSERT_RECORD,
* MultiCustomizer.of(QCodeReference(x), QCodeReference(y)));
* </pre>
*
* @param codeReferences
* one or more {@link QCodeReference objects} to run when this customizer
* runs. note that they will run in the order provided in this list.
***************************************************************************/
public static QCodeReferenceWithProperties of(QCodeReference... codeReferences)
{
ArrayList<QCodeReference> list = new ArrayList<>(Arrays.stream(codeReferences).toList());
return (new QCodeReferenceWithProperties(MultiCustomizer.class, MapBuilder.of(KEY_CODE_REFERENCES, list)));
}
/***************************************************************************
* Add an additional table customizer code reference to an existing
* codeReference, e.g., constructed by the `of` factory method.
*
* @see #of(QCodeReference...)
***************************************************************************/
public static void addTableCustomizer(QCodeReferenceWithProperties existingMultiCustomizerCodeReference, QCodeReference codeReference)
{
ArrayList<QCodeReference> list = (ArrayList<QCodeReference>) existingMultiCustomizerCodeReference.getProperties().computeIfAbsent(KEY_CODE_REFERENCES, key -> new ArrayList<>());
list.add(codeReference);
}
/***************************************************************************
* When this class is instantiated by the QCodeLoader, initialize the
* sub-customizer objects.
***************************************************************************/
@Override
public void initialize(QCodeReference codeReference)
{
if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties)
{
Serializable codeReferencesPropertyValue = codeReferenceWithProperties.getProperties().get(KEY_CODE_REFERENCES);
if(codeReferencesPropertyValue instanceof List<?> list)
{
for(Object o : list)
{
if(o instanceof QCodeReference reference)
{
TableCustomizerInterface customizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, reference);
customizers.add(customizer);
}
}
}
else
{
LOG.warn("Property KEY_CODE_REFERENCES [" + KEY_CODE_REFERENCES + "] must be a List<QCodeReference>.");
}
}
if(customizers.isEmpty())
{
LOG.info("No TableCustomizers were specified for MultiCustomizer.");
}
}
/***************************************************************************
* run postQuery method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postQuery(queryInput, records);
}
return records;
}
/***************************************************************************
* run preInsert method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preInsert(insertInput, records, isPreview);
}
return records;
}
/***************************************************************************
* run postInsert method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postInsert(insertInput, records);
}
return records;
}
/***************************************************************************
* run preUpdate method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preUpdate(updateInput, records, isPreview, oldRecordList);
}
return records;
}
/***************************************************************************
* run postUpdate method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postUpdate(updateInput, records, oldRecordList);
}
return records;
}
/***************************************************************************
* run preDelete method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preDelete(deleteInput, records, isPreview);
}
return records;
}
/***************************************************************************
* run postDelete method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postDelete(deleteInput, records);
}
return records;
}
}

View File

@ -1,88 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.customizers;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.collections.TypeTolerantKeyMap;
/*******************************************************************************
** utility class to help table customizers working with the oldRecordList.
** Usage is just 2 lines:
** outside of loop-over-records:
** - OldRecordHelper oldRecordHelper = new OldRecordHelper(updateInput.getTableName(), oldRecordList);
** then inside the record loop:
** - Optional<QRecord> oldRecord = oldRecordHelper.getOldRecord(record);
*******************************************************************************/
public class OldRecordHelper
{
private String primaryKeyField;
private QFieldType primaryKeyType;
private Optional<List<QRecord>> oldRecordList;
private Map<Serializable, QRecord> oldRecordMap;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public OldRecordHelper(String tableName, Optional<List<QRecord>> oldRecordList)
{
this.primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
this.primaryKeyType = QContext.getQInstance().getTable(tableName).getField(primaryKeyField).getType();
this.oldRecordList = oldRecordList;
}
/***************************************************************************
**
***************************************************************************/
public Optional<QRecord> getOldRecord(QRecord record)
{
if(oldRecordMap == null)
{
if(oldRecordList.isPresent())
{
oldRecordMap = new TypeTolerantKeyMap<>(primaryKeyType);
oldRecordList.get().forEach(r -> oldRecordMap.put(r.getValue(primaryKeyField), r));
}
else
{
oldRecordMap = Collections.emptyMap();
}
}
return (Optional.ofNullable(oldRecordMap.get(record.getValue(primaryKeyField))));
}
}

View File

@ -401,7 +401,6 @@ public class DeleteAction
if(CollectionUtils.nullSafeHasContents(associatedKeys))
{
DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setFlags(deleteInput.getFlags());
nextLevelDeleteInput.setTransaction(deleteInput.getTransaction());
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);

View File

@ -34,6 +34,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
@ -53,7 +54,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -157,7 +157,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
//////////////////////////////////////////////////
// insert any associations in the input records //
//////////////////////////////////////////////////
manageAssociations(table, insertOutput.getRecords(), insertInput);
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
//////////////////
// do the audit //
@ -174,21 +174,9 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
.withRecordList(insertOutput.getRecords()));
}
////////////////////////////////////////////////////////////////
// finally, run the post-insert customizers, if there are any //
////////////////////////////////////////////////////////////////
runPostInsertCustomizers(insertInput, table, insertOutput);
return insertOutput;
}
/***************************************************************************
**
***************************************************************************/
private static void runPostInsertCustomizers(InsertInput insertInput, QTableMetaData table, InsertOutput insertOutput)
{
//////////////////////////////////////////////////////////////
// finally, run the post-insert customizer, if there is one //
//////////////////////////////////////////////////////////////
Optional<TableCustomizerInterface> postInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_INSERT_RECORD.getRole());
if(postInsertCustomizer.isPresent())
{
@ -205,25 +193,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_INSERT_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
try
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
insertOutput.setRecords(tableCustomizer.postInsert(insertInput, insertOutput.getRecords()));
}
catch(Exception e)
{
for(QRecord record : insertOutput.getRecords())
{
record.addWarning(new QWarningMessage("An error occurred after the insert: " + e.getMessage()));
}
}
}
return insertOutput;
}
@ -338,19 +308,6 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
insertInput.setRecords(preInsertCustomizer.get().preInsert(insertInput, insertInput.getRecords(), isPreview));
}
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_INSERT_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
if(whenToRun.equals(tableCustomizer.whenToRunPreInsert(insertInput, isPreview)))
{
insertInput.setRecords(tableCustomizer.preInsert(insertInput, insertInput.getRecords(), isPreview));
}
}
}
@ -385,7 +342,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, InsertInput insertInput) throws QException
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, QBackendTransaction transaction) throws QException
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
@ -418,8 +375,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
if(CollectionUtils.nullSafeHasContents(nextLevelInserts))
{
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.withFlags(insertInput.getFlags());
nextLevelInsertInput.setTransaction(insertInput.getTransaction());
nextLevelInsertInput.setTransaction(transaction);
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -126,7 +126,6 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName());
insertInput.setRecords(insertList);
insertInput.withFlags(input.getFlags());
insertInput.setTransaction(transaction);
insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -136,7 +135,6 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
updateInput.setRecords(updateList);
updateInput.withFlags(input.getFlags());
updateInput.setTransaction(transaction);
updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
@ -153,7 +151,6 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter);
deleteInput.withFlags(input.getFlags());
deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);

View File

@ -57,7 +57,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -200,18 +199,6 @@ public class UpdateAction
//////////////////////////////////////////////////////////////
// finally, run the post-update customizer, if there is one //
//////////////////////////////////////////////////////////////
runPostUpdateCustomizers(updateInput, table, updateOutput, oldRecordList);
return updateOutput;
}
/***************************************************************************
**
***************************************************************************/
private static void runPostUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, UpdateOutput updateOutput, Optional<List<QRecord>> oldRecordList)
{
Optional<TableCustomizerInterface> postUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_UPDATE_RECORD.getRole());
if(postUpdateCustomizer.isPresent())
{
@ -228,49 +215,7 @@ public class UpdateAction
}
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_UPDATE_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
try
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
updateOutput.setRecords(tableCustomizer.postUpdate(updateInput, updateOutput.getRecords(), oldRecordList));
}
catch(Exception e)
{
for(QRecord record : updateOutput.getRecords())
{
record.addWarning(new QWarningMessage("An error occurred after the update: " + e.getMessage()));
}
}
}
}
/***************************************************************************
**
***************************************************************************/
private static void runPreUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, Optional<List<QRecord>> oldRecordList, boolean isPreview) throws QException
{
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_UPDATE_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
updateInput.setRecords(tableCustomizer.preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
return updateOutput;
}
@ -333,7 +278,11 @@ public class UpdateAction
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
runPreUpdateCustomizers(updateInput, table, oldRecordList, isPreview);
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
}
@ -456,7 +405,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, Collections.emptyMap(), QContext.getQSession());
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap());
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));
@ -605,7 +554,6 @@ public class UpdateAction
{
LOG.debug("Deleting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", queryOutput.getRecords().size()));
DeleteInput deleteInput = new DeleteInput();
deleteInput.setFlags(updateInput.getFlags());
deleteInput.setTransaction(updateInput.getTransaction());
deleteInput.setTableName(association.getAssociatedTableName());
deleteInput.setPrimaryKeys(queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).collect(Collectors.toList()));
@ -618,7 +566,6 @@ public class UpdateAction
LOG.debug("Updating associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
UpdateInput nextLevelUpdateInput = new UpdateInput();
nextLevelUpdateInput.setTransaction(updateInput.getTransaction());
nextLevelUpdateInput.setFlags(updateInput.getFlags());
nextLevelUpdateInput.setTableName(association.getAssociatedTableName());
nextLevelUpdateInput.setRecords(nextLevelUpdates);
UpdateOutput nextLevelUpdateOutput = new UpdateAction().execute(nextLevelUpdateInput);
@ -629,7 +576,6 @@ public class UpdateAction
LOG.debug("Inserting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(updateInput.getTransaction());
nextLevelInsertInput.setFlags(updateInput.getFlags());
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -50,7 +50,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -103,7 +102,7 @@ public class ValidateRecordSecurityLockHelper
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction, QContext.getQSession());
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction);
/////////////////////////////////
// propagate errors to records //
@ -125,29 +124,6 @@ public class ValidateRecordSecurityLockHelper
/***************************************************************************
** return boolean if given session can read given record
***************************************************************************/
public static boolean allowedToReadRecord(QTableMetaData table, QRecord record, QSession qSession, QBackendTransaction transaction) throws QException
{
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, Action.SELECT);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
{
return (true);
}
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, List.of(record), Action.SELECT, locksToCheck, errorRecords, new ArrayList<>(), Collections.emptyMap(), transaction, qSession);
if(errorRecords.containsKey(record.getValue(table.getPrimaryKeyField())))
{
return (false);
}
return (true);
}
/*******************************************************************************
** For a list of `records` from a `table`, and a given `action`, evaluate a
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
@ -166,7 +142,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, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction, QSession qSession) 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, QBackendTransaction transaction) throws QException
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
@ -177,7 +153,7 @@ public class ValidateRecordSecurityLockHelper
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction, qSession);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction);
treePosition.remove(treePosition.size() - 1);
i++;
}
@ -189,7 +165,7 @@ public class ValidateRecordSecurityLockHelper
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && qSession.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
return;
}
@ -217,7 +193,7 @@ public class ValidateRecordSecurityLockHelper
}
Serializable recordSecurityValue = record.getValue(field.getName());
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
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);
@ -363,7 +339,7 @@ public class ValidateRecordSecurityLockHelper
for(QRecord inputRecord : inputRecords)
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
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);
@ -470,7 +446,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys, QSession qSession)
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys)
{
if(recordSecurityValue == null || (madeUpPrimaryKeys != null && madeUpPrimaryKeys.containsKey(recordSecurityValue)))
{
@ -485,7 +461,7 @@ public class ValidateRecordSecurityLockHelper
}
else
{
if(!qSession.hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
if(!QContext.getQSession().hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
{
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{

View File

@ -47,12 +47,12 @@ public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializabl
/***************************************************************************
**
***************************************************************************/
protected abstract S getSourceObject(Serializable id) throws QException;
protected abstract S getSourceObject(Serializable id);
/***************************************************************************
**
***************************************************************************/
protected abstract List<S> getAllSourceObjects() throws QException;
protected abstract List<S> getAllSourceObjects();
@ -60,7 +60,7 @@ public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializabl
**
***************************************************************************/
@Override
public QPossibleValue<ID> getPossibleValue(Serializable idValue) throws QException
public QPossibleValue<ID> getPossibleValue(Serializable idValue)
{
S sourceObject = getSourceObject(idValue);
if(sourceObject == null)

View File

@ -45,7 +45,7 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
/*******************************************************************************
**
*******************************************************************************/
QPossibleValue<T> getPossibleValue(Serializable idValue) throws QException;
QPossibleValue<T> getPossibleValue(Serializable idValue);
/*******************************************************************************
**

View File

@ -47,7 +47,6 @@ import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnri
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
@ -211,11 +210,6 @@ public class QInstanceEnricher
***************************************************************************/
private void enrichInstance()
{
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : qInstance.getSupplementalMetaData().values())
{
supplementalInstanceMetaData.enrich(qInstance);
}
runPlugins(QInstance.class, qInstance, qInstance);
}
@ -1410,7 +1404,7 @@ public class QInstanceEnricher
if(possibleValueSource.getIdType() == null)
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
if(table != null && table.getFields() != null)
if(table != null)
{
String primaryKeyField = table.getPrimaryKeyField();
QFieldMetaData primaryKeyFieldMetaData = table.getFields().get(primaryKeyField);
@ -1483,18 +1477,7 @@ public class QInstanceEnricher
if(enrichMethod.isPresent())
{
Class<?> parameterType = enrichMethod.get().getParameterTypes()[0];
Set<String> existingPluginIdentifiers = enricherPlugins.getOrDefault(parameterType, Collections.emptyList())
.stream().map(p -> p.getPluginIdentifier())
.collect(Collectors.toSet());
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
{
LOG.debug("Enricher plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
}
else
{
enricherPlugins.add(parameterType, plugin);
}
enricherPlugins.add(parameterType, plugin);
}
else
{

View File

@ -42,7 +42,6 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
@ -253,17 +252,6 @@ public class QInstanceValidator
{
validateSimpleCodeReference("Instance metaDataActionCustomizer ", qInstance.getMetaDataActionCustomizer(), MetaDataActionCustomizerInterface.class);
}
if(qInstance.getTableCustomizers() != null)
{
for(Map.Entry<String, List<QCodeReference>> entry : qInstance.getTableCustomizers().entrySet())
{
for(QCodeReference codeReference : CollectionUtils.nonNullList(entry.getValue()))
{
validateSimpleCodeReference("Instance tableCustomizer of type " + entry.getKey() + ": ", codeReference, TableCustomizerInterface.class);
}
}
}
}
@ -295,18 +283,7 @@ public class QInstanceValidator
if(validateMethod.isPresent())
{
Class<?> parameterType = validateMethod.get().getParameterTypes()[0];
Set<String> existingPluginIdentifiers = validatorPlugins.getOrDefault(parameterType, Collections.emptyList())
.stream().map(p -> p.getPluginIdentifier())
.collect(Collectors.toSet());
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
{
LOG.debug("Validator plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
}
else
{
validatorPlugins.add(parameterType, plugin);
}
validatorPlugins.add(parameterType, plugin);
}
else
{
@ -326,17 +303,6 @@ public class QInstanceValidator
/*******************************************************************************
** Getter for validatorPlugins
**
*******************************************************************************/
public static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> getValidatorPlugins()
{
return validatorPlugins;
}
/*******************************************************************************
**
*******************************************************************************/
@ -2272,7 +2238,8 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
public void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
@SafeVarargs
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
{
if(!preAssertionsForCodeReference(codeReference, prefix))
{

View File

@ -1,37 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.assessment;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** marker for an object which can be processed by the QInstanceAssessor.
*******************************************************************************/
public interface Assessable
{
/***************************************************************************
**
***************************************************************************/
void assess(QInstanceAssessor qInstanceAssessor, QInstance qInstance);
}

View File

@ -1,219 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.assessment;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** POC of a class that is meant to review meta-data for accuracy vs. real backends.
*******************************************************************************/
public class QInstanceAssessor
{
private static final QLogger LOG = QLogger.getLogger(QInstanceAssessor.class);
private final QInstance qInstance;
private List<String> errors = new ArrayList<>();
private List<String> warnings = new ArrayList<>();
private List<String> suggestions = new ArrayList<>();
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QInstanceAssessor(QInstance qInstance)
{
this.qInstance = qInstance;
}
/*******************************************************************************
**
*******************************************************************************/
public void assess()
{
for(QBackendMetaData backend : qInstance.getBackends().values())
{
if(backend instanceof Assessable assessable)
{
assessable.assess(this, qInstance);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters")
public String getSummary()
{
StringBuilder rs = new StringBuilder();
///////////////////////////
// print header & errors //
///////////////////////////
if(CollectionUtils.nullSafeIsEmpty(errors))
{
rs.append("Assessment passed with no errors! \uD83D\uDE0E\n");
}
else
{
rs.append("Assessment found the following ").append(StringUtils.plural(errors, "error", "errors")).append(": \uD83D\uDE32\n");
for(String error : errors)
{
rs.append(" - ").append(error).append("\n");
}
}
/////////////////////////////////////
// print warnings if there are any //
/////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(warnings))
{
rs.append("\nAssessment found the following ").append(StringUtils.plural(warnings, "warning", "warnings")).append(": \uD83E\uDD28\n");
for(String warning : warnings)
{
rs.append(" - ").append(warning).append("\n");
}
}
//////////////////////////////////////////
// print suggestions, if there were any //
//////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(suggestions))
{
rs.append("\nThe following ").append(StringUtils.plural(suggestions, "fix is", "fixes are")).append(" suggested: \uD83E\uDD13\n");
for(String suggestion : suggestions)
{
rs.append("\n").append(suggestion).append("\n\n");
}
}
return (rs.toString());
}
/*******************************************************************************
** Getter for qInstance
**
*******************************************************************************/
public QInstance getInstance()
{
return qInstance;
}
/*******************************************************************************
** Getter for errors
**
*******************************************************************************/
public List<String> getErrors()
{
return errors;
}
/*******************************************************************************
** Getter for warnings
**
*******************************************************************************/
public List<String> getWarnings()
{
return warnings;
}
/*******************************************************************************
**
*******************************************************************************/
public void addError(String errorMessage)
{
errors.add(errorMessage);
}
/*******************************************************************************
**
*******************************************************************************/
public void addWarning(String warningMessage)
{
warnings.add(warningMessage);
}
/*******************************************************************************
**
*******************************************************************************/
public void addError(String errorMessage, Exception e)
{
addError(errorMessage + " : " + e.getMessage());
}
/*******************************************************************************
**
*******************************************************************************/
public void addSuggestion(String message)
{
suggestions.add(message);
}
/*******************************************************************************
**
*******************************************************************************/
public int getExitCode()
{
if(CollectionUtils.nullSafeHasContents(errors))
{
return (1);
}
else
{
return (0);
}
}
}

View File

@ -37,13 +37,4 @@ public interface QInstanceEnricherPluginInterface<T>
*******************************************************************************/
void enrich(T object, QInstance qInstance);
/***************************************************************************
**
***************************************************************************/
default String getPluginIdentifier()
{
return getClass().getName();
}
}

View File

@ -38,13 +38,4 @@ public interface QInstanceValidatorPluginInterface<T>
*******************************************************************************/
void validate(T object, QInstance qInstance, QInstanceValidator qInstanceValidator);
/***************************************************************************
**
***************************************************************************/
default String getPluginIdentifier()
{
return getClass().getName();
}
}

View File

@ -23,11 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -35,45 +31,6 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
*******************************************************************************/
public interface ProcessSummaryLineInterface extends Serializable
{
QLogger LOG = QLogger.getLogger(ProcessSummaryLineInterface.class);
/***************************************************************************
**
***************************************************************************/
static void log(String message, Serializable summaryLines, List<LogPair> additionalLogPairs)
{
try
{
if(summaryLines instanceof List)
{
List<ProcessSummaryLineInterface> list = (List<ProcessSummaryLineInterface>) summaryLines;
List<LogPair> logPairs = new ArrayList<>();
for(ProcessSummaryLineInterface processSummaryLineInterface : list)
{
LogPair logPair = processSummaryLineInterface.toLogPair();
logPair.setKey(logPair.getKey() + logPairs.size());
logPairs.add(logPair);
}
if(additionalLogPairs != null)
{
logPairs.addAll(0, additionalLogPairs);
}
logPairs.add(0, logPair("message", message));
LOG.info(logPairs);
}
else
{
LOG.info("Unrecognized type for summaryLines (expected List)", logPair("processSummary", summaryLines));
}
}
catch(Exception e)
{
LOG.info("Error logging a process summary", e, logPair("processSummary", summaryLines));
}
}
/*******************************************************************************
** Getter for status

View File

@ -1,35 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
import java.io.Serializable;
/*******************************************************************************
** interface to mark enums (presumably classes too, but the original intent is
** enums) that can be added to insert/update/delete action inputs to flag behaviors
*******************************************************************************/
public interface ActionFlag extends Serializable
{
}

View File

@ -24,12 +24,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -50,8 +47,6 @@ public class DeleteInput extends AbstractTableActionInput
private boolean omitDmlAudit = false;
private String auditContext = null;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -300,65 +295,4 @@ public class DeleteInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public DeleteInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public DeleteInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -23,12 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -51,8 +48,6 @@ public class InsertInput extends AbstractTableActionInput
private boolean omitDmlAudit = false;
private String auditContext = null;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -321,65 +316,4 @@ public class InsertInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public InsertInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public InsertInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -22,12 +22,9 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
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.tables.UniqueKey;
@ -42,14 +39,12 @@ public class ReplaceInput extends AbstractTableActionInput
private UniqueKey key;
private List<QRecord> records;
private QQueryFilter filter;
private boolean performDeletes = true;
private boolean allowNullKeyValuesToEqual = false;
private boolean setPrimaryKeyInInsertedRecords = false;
private boolean performDeletes = true;
private boolean allowNullKeyValuesToEqual = false;
private boolean setPrimaryKeyInInsertedRecords = false;
private boolean omitDmlAudit = false;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -308,65 +303,4 @@ public class ReplaceInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public ReplaceInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public ReplaceInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -23,12 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.update;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -59,8 +56,6 @@ public class UpdateInput extends AbstractTableActionInput
private boolean omitModifyDateUpdate = false;
private String auditContext = null;
private Set<ActionFlag> flags;
/*******************************************************************************
@ -390,65 +385,4 @@ public class UpdateInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for flags
*******************************************************************************/
public Set<ActionFlag> getFlags()
{
return (this.flags);
}
/*******************************************************************************
** Setter for flags
*******************************************************************************/
public void setFlags(Set<ActionFlag> flags)
{
this.flags = flags;
}
/*******************************************************************************
** Fluent setter for flags
*******************************************************************************/
public UpdateInput withFlags(Set<ActionFlag> flags)
{
this.flags = flags;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public UpdateInput withFlag(ActionFlag flag)
{
if(this.flags == null)
{
this.flags = new HashSet<>();
}
this.flags.add(flag);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasFlag(ActionFlag flag)
{
if(this.flags == null)
{
return (false);
}
return (this.flags.contains(flag));
}
}

View File

@ -594,6 +594,7 @@ public abstract class QRecordEntity
{
Field tableNameField = entityClass.getDeclaredField("TABLE_NAME");
String tableNameValue = (String) tableNameField.get(null);
return (tableNameValue);
}
catch(Exception e)

View File

@ -177,18 +177,6 @@ public class MetaDataProducerHelper
/////////////////////////////////////////////////////////////////////////////////////////////
// sort them by sort order, then by the type that they return, as set up in the static map //
/////////////////////////////////////////////////////////////////////////////////////////////
sortMetaDataProducers(producers);
return (producers);
}
/***************************************************************************
**
***************************************************************************/
public static void sortMetaDataProducers(List<MetaDataProducerInterface<?>> producers)
{
producers.sort(Comparator
.comparing((MetaDataProducerInterface<?> p) -> p.getSortOrder())
.thenComparing((MetaDataProducerInterface<?> p) ->
@ -203,8 +191,9 @@ public class MetaDataProducerHelper
return (0);
}
}));
}
return (producers);
}
/*******************************************************************************
@ -428,7 +417,7 @@ public class MetaDataProducerHelper
return (null);
}
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy(), childTable.childJoin().isOneToOne());
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy());
producer.setSourceClass(entityClass);
return producer;
}

View File

@ -86,7 +86,7 @@ public class MetaDataProducerMultiOutput implements MetaDataProducerOutput, Sour
{
List<T> rs = new ArrayList<>();
for(MetaDataProducerOutput content : CollectionUtils.nonNullList(contents))
for(MetaDataProducerOutput content : contents)
{
if(content instanceof MetaDataProducerMultiOutput multiOutput)
{

View File

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@ -31,7 +30,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
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;
@ -67,7 +65,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry;
@ -119,8 +116,6 @@ public class QInstance
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
private ListingHash<String, QCodeReference> tableCustomizers;
@Deprecated(since = "migrated to metaDataCustomizer")
private QCodeReference metaDataFilter = null;
@ -1628,76 +1623,4 @@ public class QInstance
return (this);
}
/*******************************************************************************
** Getter for tableCustomizers
*******************************************************************************/
public ListingHash<String, QCodeReference> getTableCustomizers()
{
return (this.tableCustomizers);
}
/*******************************************************************************
** Setter for tableCustomizers
*******************************************************************************/
public void setTableCustomizers(ListingHash<String, QCodeReference> tableCustomizers)
{
this.tableCustomizers = tableCustomizers;
}
/*******************************************************************************
** Fluent setter for tableCustomizers
*******************************************************************************/
public QInstance withTableCustomizers(ListingHash<String, QCodeReference> tableCustomizers)
{
this.tableCustomizers = tableCustomizers;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QInstance withTableCustomizer(String role, QCodeReference customizer)
{
if(this.tableCustomizers == null)
{
this.tableCustomizers = new ListingHash<>();
}
this.tableCustomizers.add(role, customizer);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QInstance withTableCustomizer(TableCustomizers tableCustomizer, QCodeReference customizer)
{
return (withTableCustomizer(tableCustomizer.getRole(), customizer));
}
/*******************************************************************************
** Getter for tableCustomizers
*******************************************************************************/
public List<QCodeReference> getTableCustomizers(TableCustomizers tableCustomizer)
{
if(this.tableCustomizers == null)
{
return (Collections.emptyList());
}
return (this.tableCustomizers.getOrDefault(tableCustomizer.getRole(), Collections.emptyList()));
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
@ -36,7 +37,7 @@ public interface QSupplementalInstanceMetaData extends TopLevelMetaDataInterface
/*******************************************************************************
**
*******************************************************************************/
default void enrich(QInstance qInstance)
default void enrich(QTableMetaData table)
{
////////////////////////
// noop in base class //

View File

@ -26,15 +26,12 @@ import java.util.Objects;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@ -65,7 +62,6 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
private String childTableName; // e.g., lineItem
private String parentTableName; // e.g., order
private String foreignKeyFieldName; // e.g., orderId
private boolean isOneToOne;
private ChildJoin.OrderBy[] orderBys;
@ -76,7 +72,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
/***************************************************************************
**
***************************************************************************/
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys, boolean isOneToOne)
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys)
{
Objects.requireNonNull(childTableName, "childTableName cannot be null");
Objects.requireNonNull(parentTableName, "parentTableName cannot be null");
@ -86,7 +82,6 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
this.parentTableName = parentTableName;
this.foreignKeyFieldName = foreignKeyFieldName;
this.orderBys = orderBys;
this.isOneToOne = isOneToOne;
}
@ -97,14 +92,23 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
@Override
public QJoinMetaData produce(QInstance qInstance) throws QException
{
QTableMetaData parentTable = getTable(qInstance, parentTableName);
QTableMetaData childTable = getTable(qInstance, childTableName);
QTableMetaData parentTable = qInstance.getTable(parentTableName);
if(parentTable == null)
{
throw (new QException("Could not find tableMetaData " + parentTableName));
}
QTableMetaData childTable = qInstance.getTable(childTableName);
if(childTable == null)
{
throw (new QException("Could not find tableMetaData " + childTable));
}
QJoinMetaData join = new QJoinMetaData()
.withLeftTable(parentTableName)
.withRightTable(childTableName)
.withInferredName()
.withType(isOneToOne ? JoinType.ONE_TO_ONE : JoinType.ONE_TO_MANY)
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn(parentTable.getPrimaryKeyField(), foreignKeyFieldName));
if(orderBys != null && orderBys.length > 0)
@ -127,41 +131,6 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
/***************************************************************************
*
***************************************************************************/
private QTableMetaData getTable(QInstance qInstance, String tableName) throws QException
{
QTableMetaData table = qInstance.getTable(tableName);
if(table == null)
{
///////////////////////////////////////////////////////////////////////////////
// in case we're producing a QBit, and it's added a table to a multi-output, //
// but not yet the instance, see if we can get table from there //
///////////////////////////////////////////////////////////////////////////////
for(MetaDataProducerMultiOutput metaDataProducerMultiOutput : QBitProductionContext.getReadOnlyViewOfMetaDataProducerMultiOutputStack())
{
table = CollectionUtils.nonNullList(metaDataProducerMultiOutput.getEach(QTableMetaData.class)).stream()
.filter(t -> t.getName().equals(tableName))
.findFirst().orElse(null);
if(table != null)
{
break;
}
}
}
if(table == null)
{
throw (new QException("Could not find tableMetaData: " + table));
}
return table;
}
/*******************************************************************************
** Getter for sourceClass
**

View File

@ -25,13 +25,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -92,26 +89,6 @@ public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implem
String name = QJoinMetaData.makeInferredJoinName(parentTableName, childTableName);
QJoinMetaData join = qInstance.getJoin(name);
if(join == null)
{
for(MetaDataProducerMultiOutput metaDataProducerMultiOutput : QBitProductionContext.getReadOnlyViewOfMetaDataProducerMultiOutputStack())
{
join = CollectionUtils.nonNullList(metaDataProducerMultiOutput.getEach(QJoinMetaData.class)).stream()
.filter(t -> t.getName().equals(name))
.findFirst().orElse(null);
if(join != null)
{
break;
}
}
}
if(join == null)
{
throw (new QException("Could not find joinMetaData: " + name));
}
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(join)
.withName(name)
.withLabel(childRecordListWidget.label())

View File

@ -38,8 +38,6 @@ public @interface ChildJoin
OrderBy[] orderBy() default { };
boolean isOneToOne() default false;
/***************************************************************************
**
***************************************************************************/

View File

@ -107,14 +107,4 @@ public interface QBitConfig extends Serializable
{
return (null);
}
/***************************************************************************
*
***************************************************************************/
default String getDefaultBackendNameForTables()
{
return (null);
}
}

View File

@ -1,192 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** 2nd generation interface for top-level meta-data production classes that make
** a qbit (evolution over QBitProducer).
**
*******************************************************************************/
public interface QBitMetaDataProducer<C extends QBitConfig> extends MetaDataProducerInterface<MetaDataProducerMultiOutput>
{
QLogger LOG = QLogger.getLogger(QBitMetaDataProducer.class);
/***************************************************************************
**
***************************************************************************/
C getQBitConfig();
/***************************************************************************
**
***************************************************************************/
QBitMetaData getQBitMetaData();
/***************************************************************************
**
***************************************************************************/
default String getNamespace()
{
return (null);
}
/***************************************************************************
**
***************************************************************************/
default void postProduceActions(MetaDataProducerMultiOutput metaDataProducerMultiOutput, QInstance qinstance)
{
/////////////////////
// noop by default //
/////////////////////
}
/***************************************************************************
**
***************************************************************************/
default String getPackageNameForFindingMetaDataProducers()
{
Class<?> clazz = getClass();
////////////////////////////////////////////////////////////////
// Walk up the hierarchy until we find the direct implementer //
////////////////////////////////////////////////////////////////
while(clazz != null)
{
Class<?>[] interfaces = clazz.getInterfaces();
for(Class<?> interfaze : interfaces)
{
if(interfaze == QBitMetaDataProducer.class)
{
return clazz.getPackageName();
}
}
clazz = clazz.getSuperclass();
}
throw (new QRuntimeException("Unable to find packageName for QBitMetaDataProducer. You may need to implement getPackageName yourself..."));
}
/***************************************************************************
**
***************************************************************************/
@Override
default MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
{
MetaDataProducerMultiOutput rs = new MetaDataProducerMultiOutput();
QBitMetaData qBitMetaData = getQBitMetaData();
C qBitConfig = getQBitConfig();
qInstance.addQBit(qBitMetaData);
QBitProductionContext.pushQBitConfig(qBitConfig);
QBitProductionContext.pushMetaDataProducerMultiOutput(rs);
try
{
qBitConfig.validate(qInstance);
List<MetaDataProducerInterface<?>> producers = MetaDataProducerHelper.findProducers(getPackageNameForFindingMetaDataProducers());
MetaDataProducerHelper.sortMetaDataProducers(producers);
for(MetaDataProducerInterface<?> producer : producers)
{
if(producer.getClass().equals(this.getClass()))
{
/////////////////////////////////////////////
// avoid recursive processing of ourselves //
/////////////////////////////////////////////
continue;
}
////////////////////////////////////////////////////////////////////////////
// todo is this deprecated in favor of QBitProductionContext's stack... ? //
////////////////////////////////////////////////////////////////////////////
if(producer instanceof QBitComponentMetaDataProducer<?, ?>)
{
QBitComponentMetaDataProducer<?, C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducer<?, C>) producer;
qBitComponentMetaDataProducer.setQBitConfig(qBitConfig);
}
if(!producer.isEnabled())
{
LOG.debug("Not using producer which is not enabled", logPair("producer", producer.getClass().getSimpleName()));
continue;
}
MetaDataProducerOutput subProducerOutput = producer.produce(qInstance);
/////////////////////////////////////////////////
// apply some things from the config to tables //
/////////////////////////////////////////////////
if(subProducerOutput instanceof QTableMetaData table)
{
if(qBitConfig.getTableMetaDataCustomizer() != null)
{
subProducerOutput = qBitConfig.getTableMetaDataCustomizer().customizeMetaData(qInstance, table);
}
if(!StringUtils.hasContent(table.getBackendName()) && StringUtils.hasContent(qBitConfig.getDefaultBackendNameForTables()))
{
table.setBackendName(qBitConfig.getDefaultBackendNameForTables());
}
}
////////////////////////////////////////////////////////////
// set source qbit, if subProducerOutput is aware of such //
////////////////////////////////////////////////////////////
if(subProducerOutput instanceof SourceQBitAware sourceQBitAware)
{
sourceQBitAware.setSourceQBitName(qBitMetaData.getName());
}
rs.add(subProducerOutput);
}
postProduceActions(rs, qInstance);
return (rs);
}
finally
{
QBitProductionContext.popQBitConfig();
QBitProductionContext.popMetaDataProducerMultiOutput();
}
}
}

View File

@ -76,6 +76,9 @@ public interface QBitProducer
{
qBitConfig.validate(qInstance);
///////////////////////////////
// todo - move to base class //
///////////////////////////////
for(MetaDataProducerInterface<?> producer : producers)
{
if(producer instanceof QBitComponentMetaDataProducer<?, ?>)

View File

@ -1,136 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
/*******************************************************************************
** While a qbit is being produced, track the context of the current config
** and metaDataProducerMultiOutput that is being used. also, in case one
** qbit produces another, push these contextual objects on a stack.
*******************************************************************************/
public class QBitProductionContext
{
private static final QLogger LOG = QLogger.getLogger(QBitProductionContext.class);
private static Stack<QBitConfig> qbitConfigStack = new Stack<>();
private static Stack<MetaDataProducerMultiOutput> metaDataProducerMultiOutputStack = new Stack<>();
/***************************************************************************
**
***************************************************************************/
public static void pushQBitConfig(QBitConfig qBitConfig)
{
qbitConfigStack.push(qBitConfig);
}
/***************************************************************************
**
***************************************************************************/
public static QBitConfig peekQBitConfig()
{
if(qbitConfigStack.isEmpty())
{
LOG.warn("Request to peek at empty QBitProductionContext configStack - returning null");
return (null);
}
return qbitConfigStack.peek();
}
/***************************************************************************
**
***************************************************************************/
public static void popQBitConfig()
{
if(qbitConfigStack.isEmpty())
{
LOG.warn("Request to pop empty QBitProductionContext configStack - returning with noop");
return;
}
qbitConfigStack.pop();
}
/***************************************************************************
**
***************************************************************************/
public static void pushMetaDataProducerMultiOutput(MetaDataProducerMultiOutput metaDataProducerMultiOutput)
{
metaDataProducerMultiOutputStack.push(metaDataProducerMultiOutput);
}
/***************************************************************************
**
***************************************************************************/
public static MetaDataProducerMultiOutput peekMetaDataProducerMultiOutput()
{
if(metaDataProducerMultiOutputStack.isEmpty())
{
LOG.warn("Request to peek at empty QBitProductionContext configStack - returning null");
return (null);
}
return metaDataProducerMultiOutputStack.peek();
}
/***************************************************************************
**
***************************************************************************/
public static List<MetaDataProducerMultiOutput> getReadOnlyViewOfMetaDataProducerMultiOutputStack()
{
return Collections.unmodifiableList(metaDataProducerMultiOutputStack);
}
/***************************************************************************
**
***************************************************************************/
public static void popMetaDataProducerMultiOutput()
{
if(metaDataProducerMultiOutputStack.isEmpty())
{
LOG.warn("Request to pop empty QBitProductionContext metaDataProducerMultiOutput - returning with noop");
return;
}
metaDataProducerMultiOutputStack.pop();
}
}

View File

@ -44,7 +44,7 @@ public class MultiRecordSecurityLock extends RecordSecurityLock implements Clone
**
*******************************************************************************/
@Override
public MultiRecordSecurityLock clone()
protected MultiRecordSecurityLock clone() throws CloneNotSupportedException
{
MultiRecordSecurityLock clone = (MultiRecordSecurityLock) super.clone();

View File

@ -57,27 +57,20 @@ public class RecordSecurityLock implements Cloneable
**
*******************************************************************************/
@Override
public RecordSecurityLock clone()
protected RecordSecurityLock clone() throws CloneNotSupportedException
{
try
{
RecordSecurityLock clone = (RecordSecurityLock) super.clone();
RecordSecurityLock clone = (RecordSecurityLock) super.clone();
/////////////////////////
// deep-clone the list //
/////////////////////////
if(joinNameChain != null)
{
clone.joinNameChain = new ArrayList<>();
clone.joinNameChain.addAll(joinNameChain);
}
return (clone);
}
catch(CloneNotSupportedException e)
/////////////////////////
// deep-clone the list //
/////////////////////////
if(joinNameChain != null)
{
throw (new RuntimeException("Could not clone", e));
clone.joinNameChain = new ArrayList<>();
clone.joinNameChain.addAll(joinNameChain);
}
return (clone);
}

View File

@ -1,123 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.BasicCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** possible-value source provider for the `QQQ Table` PVS - a list of all tables
** in an application/qInstance (that you have permission to see)
*******************************************************************************/
public class QQQTableCustomPossibleValueProvider extends BasicCustomPossibleValueProvider<QRecord, Integer>
{
/***************************************************************************
**
***************************************************************************/
@Override
protected QPossibleValue<Integer> makePossibleValue(QRecord sourceObject)
{
return (new QPossibleValue<>(sourceObject.getValueInteger("id"), sourceObject.getValueString("label")));
}
/***************************************************************************
**
***************************************************************************/
@Override
protected QRecord getSourceObject(Serializable id) throws QException
{
QRecord qqqTableRecord = GetAction.execute(QQQTable.TABLE_NAME, id);
if(qqqTableRecord == null)
{
return (null);
}
QTableMetaData table = QContext.getQInstance().getTable(qqqTableRecord.getValueString("name"));
return isTableAllowed(table) ? qqqTableRecord : null;
}
/***************************************************************************
**
***************************************************************************/
@Override
protected List<QRecord> getAllSourceObjects() throws QException
{
List<QRecord> records = QueryAction.execute(QQQTable.TABLE_NAME, null);
ArrayList<QRecord> rs = new ArrayList<>();
for(QRecord record : records)
{
QTableMetaData table = QContext.getQInstance().getTable(record.getValueString("name"));
if(isTableAllowed(table))
{
rs.add(record);
}
}
return rs;
}
/***************************************************************************
**
***************************************************************************/
private boolean isTableAllowed(QTableMetaData table)
{
if(table == null)
{
return (false);
}
if(table.getIsHidden())
{
return (false);
}
PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table);
if(!PermissionCheckResult.ALLOW.equals(permissionCheckResult))
{
return (false);
}
return (true);
}
}

View File

@ -22,29 +22,18 @@
package com.kingsrook.qqq.backend.core.model.tables;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
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.QueryOrGetInputInterface;
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.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
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;
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.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -100,41 +89,4 @@ public class QQQTableTableManager
return getOutput.getRecord().getValueInteger("id");
}
/***************************************************************************
**
***************************************************************************/
public static List<QRecord> setRecordLinksToRecordsFromTableDynamicForPostQuery(QueryOrGetInputInterface queryInput, List<QRecord> records, String tableIdField, String recordIdField) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////
// note, this is a second copy of this logic (first being in standard process traces). //
// let the rule of 3 apply if we find ourselves copying it again //
/////////////////////////////////////////////////////////////////////////////////////////
if(queryInput.getShouldGenerateDisplayValues())
{
///////////////////////////////////////////////////////////////////////////////////////////
// for records with a table id value - look up that table name, then set a display-value //
// for the Link type adornment, to the inputRecordId record within that table. //
///////////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> tableIds = records.stream().map(r -> r.getValue(tableIdField)).filter(Objects::nonNull).collect(Collectors.toSet());
if(!tableIds.isEmpty())
{
Map<Serializable, QRecord> tableMap = GeneralProcessUtils.loadTableToMap(QQQTable.TABLE_NAME, "id", new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, tableIds)));
for(QRecord record : records)
{
QRecord qqqTableRecord = tableMap.get(record.getValue(tableIdField));
if(qqqTableRecord != null && record.getValue(recordIdField) != null)
{
record.setDisplayValue(recordIdField + ":" + AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, qqqTableRecord.getValueString("name"));
}
}
}
}
return (records);
}
}

View File

@ -27,9 +27,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
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.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
@ -128,11 +125,10 @@ public class QQQTablesMetaDataProvider
public QPossibleValueSource defineQQQTablePossibleValueSource()
{
return (new QPossibleValueSource()
.withType(QPossibleValueSourceType.TABLE)
.withName(QQQTable.TABLE_NAME)
.withType(QPossibleValueSourceType.CUSTOM)
.withIdType(QFieldType.INTEGER)
.withCustomCodeReference(new QCodeReference(QQQTableCustomPossibleValueProvider.class))
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
.withTableName(QQQTable.TABLE_NAME))
.withOrderByField("label");
}
}

View File

@ -74,7 +74,6 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -365,9 +364,6 @@ public class MemoryRecordStore
// differently from other backends, because of having the same record variable in the backend store and in the user-code. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord recordToInsert = new QRecord(record);
makeValueTypesMatchFieldTypes(table, recordToInsert);
if(CollectionUtils.nullSafeHasContents(recordToInsert.getErrors()))
{
outputRecords.add(recordToInsert);
@ -418,30 +414,6 @@ public class MemoryRecordStore
/***************************************************************************
**
***************************************************************************/
private static void makeValueTypesMatchFieldTypes(QTableMetaData table, QRecord recordToInsert)
{
for(QFieldMetaData field : table.getFields().values())
{
Serializable value = recordToInsert.getValue(field.getName());
if(value != null)
{
try
{
recordToInsert.setValue(field.getName(), ValueUtils.getValueAsFieldType(field.getType(), value));
}
catch(Exception e)
{
LOG.info("Error converting value to field's type", e, logPair("fieldName", field.getName()), logPair("value", value));
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -472,19 +444,7 @@ public class MemoryRecordStore
QRecord recordToUpdate = tableData.get(primaryKeyValue);
for(Map.Entry<String, Serializable> valueEntry : record.getValues().entrySet())
{
String fieldName = valueEntry.getKey();
try
{
///////////////////////////////////////////////
// try to make field values match field type //
///////////////////////////////////////////////
recordToUpdate.setValue(fieldName, ValueUtils.getValueAsFieldType(table.getField(fieldName).getType(), valueEntry.getValue()));
}
catch(Exception e)
{
LOG.info("Error converting value to field's type", e, logPair("fieldName", fieldName), logPair("value", valueEntry.getValue()));
recordToUpdate.setValue(fieldName, valueEntry.getValue());
}
recordToUpdate.setValue(valueEntry.getKey(), valueEntry.getValue());
}
if(returnUpdatedRecords)

View File

@ -1,276 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.scheduler.processes;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
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.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
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;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QSchedulerMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.tablesync.AbstractTableSyncTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.tablesync.TableSyncProcess;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
/*******************************************************************************
* Base class to manage creating scheduled jobs based on records in another table
*
* Expected to be used via BaseSyncToScheduledJobTableCustomizer - see its javadoc.
* @see BaseSyncToScheduledJobTableCustomizer
*******************************************************************************/
public abstract class AbstractRecordSyncToScheduledJobProcess extends AbstractTableSyncTransformStep implements MetaDataProducerInterface<QProcessMetaData>
{
private static final QLogger LOG = QLogger.getLogger(AbstractRecordSyncToScheduledJobProcess.class);
public static final String SCHEDULER_NAME_FIELD_NAME = "schedulerName";
/***************************************************************************
* action flags that can be put in an insert/update/delete input to control
* behavior of this process.
***************************************************************************/
public enum ActionFlags implements ActionFlag
{
/***************************************************************************
* tell this process not to run upon such an action taken on the source table.
***************************************************************************/
DO_NOT_SYNC
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
QProcessMetaData processMetaData = TableSyncProcess.processMetaDataBuilder(false)
.withName(getClass().getSimpleName())
.withSyncTransformStepClass(getClass())
.withReviewStepRecordFields(List.of(
new QFieldMetaData(getRecordForeignKeyFieldName(), QFieldType.INTEGER).withPossibleValueSourceName(getRecordForeignKeyPossibleValueSourceName()),
new QFieldMetaData("cronExpression", QFieldType.STRING),
new QFieldMetaData("isActive", QFieldType.BOOLEAN)
))
.getProcessMetaData();
processMetaData.getBackendStep(StreamedETLWithFrontendProcess.STEP_NAME_PREVIEW).getInputMetaData()
.withField(new QFieldMetaData(SCHEDULER_NAME_FIELD_NAME, QFieldType.STRING));
return (processMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QRecord populateRecordToStore(RunBackendStepInput runBackendStepInput, QRecord destinationRecord, QRecord sourceRecord) throws QException
{
ScheduledJob scheduledJob;
if(destinationRecord == null || destinationRecord.getValue("id") == null)
{
QInstance qInstance = QContext.getQInstance();
////////////////////////////////////////////////////////////////
// this is the table at which the scheduled job will point to //
////////////////////////////////////////////////////////////////
QTableMetaData sourceTableMetaData = qInstance.getTable(getSourceTableName());
String sourceTableId = String.valueOf(sourceRecord.getValueString(sourceTableMetaData.getPrimaryKeyField()));
String sourceTableJobKey = getSourceTableName() + "Id";
///////////////////////////////////////////////////////////
// this is the table that the scheduled record points to //
///////////////////////////////////////////////////////////
QTableMetaData recordForeignTableMetaData = qInstance.getTable(getRecordForeignKeyPossibleValueSourceName());
String sourceRecordForeignKeyId = sourceRecord.getValueString(getRecordForeignKeyFieldName());
////////////////////////////////////////////////////////////////////////
// need to do an insert - set lots of key values in the scheduled job //
////////////////////////////////////////////////////////////////////////
scheduledJob = new ScheduledJob();
scheduledJob.setSchedulerName(runBackendStepInput.getValueString(SCHEDULER_NAME_FIELD_NAME));
scheduledJob.setType(ScheduledJobType.PROCESS.name());
scheduledJob.setForeignKeyType(getSourceTableName());
scheduledJob.setForeignKeyValue(sourceTableId);
scheduledJob.setJobParameters(ListBuilder.of(
new ScheduledJobParameter().withKey("isScheduledJob").withValue("true"),
new ScheduledJobParameter().withKey("processName").withValue(getProcessNameScheduledJobParameter()),
new ScheduledJobParameter().withKey(sourceTableJobKey).withValue(sourceTableId),
new ScheduledJobParameter().withKey("recordId").withValue(ValueUtils.getValueAsString(sourceRecordForeignKeyId))
));
//////////////////////////////////////////////////////////////////////////
// make a call to allow subclasses to customize parts of the job record //
//////////////////////////////////////////////////////////////////////////
scheduledJob.setLabel(recordForeignTableMetaData.getLabel() + " " + sourceRecordForeignKeyId);
scheduledJob.setDescription("Job to run " + sourceTableMetaData.getLabel() + " Id " + sourceTableId
+ " (which runs for " + recordForeignTableMetaData.getLabel() + " Id " + sourceRecordForeignKeyId + ")");
}
else
{
//////////////////////////////////////////////////////////////////////////////////
// else doing an update - populate scheduled job entity from destination record //
//////////////////////////////////////////////////////////////////////////////////
scheduledJob = new ScheduledJob(destinationRecord);
}
//////////////////////////////////////////////////////////////////////////////////
// these fields sync on insert and update //
// todo - if no diffs, should we return null (to avoid changing quartz at all?) //
//////////////////////////////////////////////////////////////////////////////////
scheduledJob.setCronExpression(sourceRecord.getValueString("cronExpression"));
scheduledJob.setCronTimeZoneId(sourceRecord.getValueString("cronTimeZoneId"));
scheduledJob.setIsActive(true);
scheduledJob = customizeScheduledJob(scheduledJob, sourceRecord);
////////////////////////////////////////////////////////////////////
// try to make sure scheduler name is set (and fail if it isn't!) //
////////////////////////////////////////////////////////////////////
makeSureSchedulerNameIsSet(scheduledJob);
return scheduledJob.toQRecord();
}
/***************************************************************************
**
***************************************************************************/
protected void makeSureSchedulerNameIsSet(ScheduledJob scheduledJob) throws QException
{
if(!StringUtils.hasContent(scheduledJob.getSchedulerName()))
{
Map<String, QSchedulerMetaData> schedulers = QContext.getQInstance().getSchedulers();
if(schedulers.size() == 1)
{
scheduledJob.setSchedulerName(schedulers.keySet().iterator().next());
}
}
if(!StringUtils.hasContent(scheduledJob.getSchedulerName()))
{
String message = "Could not determine scheduler name for webhook scheduled job.";
LOG.warn(message);
throw (new QException(message));
}
}
/*******************************************************************************
**
*******************************************************************************/
protected ScheduledJob customizeScheduledJob(ScheduledJob scheduledJob, QRecord sourceRecord) throws QException
{
return (scheduledJob);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
protected QQueryFilter getExistingRecordQueryFilter(RunBackendStepInput runBackendStepInput, List<Serializable> sourceKeyList)
{
return super.getExistingRecordQueryFilter(runBackendStepInput, sourceKeyList)
.withCriteria(new QFilterCriteria("foreignKeyType", QCriteriaOperator.EQUALS, getScheduledJobForeignKeyType()));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
protected SyncProcessConfig getSyncProcessConfig()
{
return new SyncProcessConfig(getSourceTableName(), getSourceTableKeyField(), ScheduledJob.TABLE_NAME, "foreignKeyValue", true, true);
}
/*******************************************************************************
**
*******************************************************************************/
protected abstract String getScheduledJobForeignKeyType();
/*******************************************************************************
**
*******************************************************************************/
protected abstract String getRecordForeignKeyFieldName();
/*******************************************************************************
**
*******************************************************************************/
protected abstract String getRecordForeignKeyPossibleValueSourceName();
/*******************************************************************************
**
*******************************************************************************/
protected abstract String getSourceTableName();
/*******************************************************************************
**
*******************************************************************************/
protected abstract String getProcessNameScheduledJobParameter();
/*******************************************************************************
**
*******************************************************************************/
protected String getSourceTableKeyField()
{
return ("id");
}
}

View File

@ -1,387 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.scheduler.processes;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
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.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
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.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** an implementation of a TableCustomizer that runs a subclass of
** AbstractRecordSyncToScheduledJobProcess - to manage scheduledJob records that
** correspond to records in another table (e.g., a job for each Client)
**
** Easiest way to use is:
** - BaseSyncToScheduledJobTableCustomizer.setTableCustomizers(tableMetaData, new YourSyncScheduledJobProcessSubclass());
** which adds post-insert, -update, and -delete customizers to your table.
**
** If you need additional table customizer code in those slots, I suppose you could
** simply make your customizer create an instance of this class, set its
** properties, and run its appropriate postInsertOrUpdate/postDelete methods.
*******************************************************************************/
public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInterface, InitializableViaCodeReference
{
private static final QLogger LOG = QLogger.getLogger(BaseSyncToScheduledJobTableCustomizer.class);
public static final String KEY_TABLE_NAME = "tableName";
public static final String KEY_SYNC_PROCESS_NAME = "syncProcessName";
public static final String KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE = "scheduledJobForeignKeyType";
private String tableName;
private String syncProcessName;
private String scheduledJobForeignKeyType;
/***************************************************************************
* Create a {@link QCodeReferenceWithProperties} that can be used to add this
* class to a table.
*
* If this is the only customizer for the post insert/update/delete events
* on your table, you can instead call setTableCustomizers. But if you want,
* for example, a sync-scheduled-job (what this customizer does) plus some other
* customizers, then you can call this method to get a code reference that you
* can add, for example, to {@link com.kingsrook.qqq.backend.core.actions.customizers.MultiCustomizer}
*
* @param tableMetaData the table that the customizer will be used on.
* @param syncProcess instance of the subclass of AbstractRecordSyncToScheduledJobProcess
* that should run in the table's post insert/update/delete
* events.
* @see #setTableCustomizers(QTableMetaData, AbstractRecordSyncToScheduledJobProcess)
***************************************************************************/
public static QCodeReferenceWithProperties makeCodeReference(QTableMetaData tableMetaData, AbstractRecordSyncToScheduledJobProcess syncProcess)
{
return new QCodeReferenceWithProperties(BaseSyncToScheduledJobTableCustomizer.class, Map.of(
KEY_TABLE_NAME, tableMetaData.getName(),
KEY_SYNC_PROCESS_NAME, syncProcess.getClass().getSimpleName(),
KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE, syncProcess.getScheduledJobForeignKeyType()
));
}
/***************************************************************************
* Add post insert/update/delete customizers to a table, that will run a
* sync-scheduled-job process.
*
* @param tableMetaData the table that the customizer will be used on.
* @param syncProcess instance of the subclass of AbstractRecordSyncToScheduledJobProcess
* that should run in the table's post insert/update/delete
* events.
***************************************************************************/
public static void setTableCustomizers(QTableMetaData tableMetaData, AbstractRecordSyncToScheduledJobProcess syncProcess)
{
QCodeReference codeReference = makeCodeReference(tableMetaData, syncProcess);
tableMetaData.withCustomizer(TableCustomizers.POST_INSERT_RECORD, codeReference);
tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, codeReference);
tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, codeReference);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void initialize(QCodeReference codeReference)
{
if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties)
{
tableName = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get(KEY_TABLE_NAME));
syncProcessName = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get(KEY_SYNC_PROCESS_NAME));
scheduledJobForeignKeyType = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get(KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE));
if(!StringUtils.hasContent(tableName))
{
LOG.warn("Missing property under KEY_TABLE_NAME [" + KEY_TABLE_NAME + "] in codeReference for BaseSyncToScheduledJobTableCustomizer");
}
if(!StringUtils.hasContent(syncProcessName))
{
LOG.warn("Missing property under KEY_SYNC_PROCESS_NAME [" + KEY_SYNC_PROCESS_NAME + "] in codeReference for BaseSyncToScheduledJobTableCustomizer");
}
if(!StringUtils.hasContent(scheduledJobForeignKeyType))
{
LOG.warn("Missing property under KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE [" + KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE + "] in codeReference for BaseSyncToScheduledJobTableCustomizer");
}
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
if(input instanceof UpdateInput updateInput && updateInput.hasFlag(AbstractRecordSyncToScheduledJobProcess.ActionFlags.DO_NOT_SYNC))
{
return records;
}
if(input instanceof InsertInput insertInput && insertInput.hasFlag(AbstractRecordSyncToScheduledJobProcess.ActionFlags.DO_NOT_SYNC))
{
return records;
}
runSyncProcessForRecordList(records, syncProcessName);
return records;
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
deleteScheduledJobsForRecordList(records);
return records;
}
/***************************************************************************
* Run the named process over a set of records (e.g., that were inserted or
* updated).
*
* This method is normally called from within this class, in postInsertOrUpdate.
*
* Note that if the {@link ScheduledJob} table isn't defined in the QInstance,
* that the process will not be called.
*
* @param records list of records to use as source records in the table-sync
* to the scheduledJob table.
* @param processName name of the sync-process to run.
***************************************************************************/
public void runSyncProcessForRecordList(List<QRecord> records, String processName)
{
if(QContext.getQInstance().getTable(ScheduledJob.TABLE_NAME) == null)
{
LOG.info("ScheduledJob table not found, skipping scheduled job sync.");
return;
}
String primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
List<Serializable> sourceRecordIds = records.stream()
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
.map(r -> r.getValue(primaryKeyField))
.filter(Objects::nonNull).toList();
if(CollectionUtils.nullSafeIsEmpty(sourceRecordIds))
{
return;
}
try
{
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(processName);
runProcessInput.setCallback(QProcessCallbackFactory.forPrimaryKeys("id", sourceRecordIds));
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
Serializable processSummary = runProcessOutput.getValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY);
ProcessSummaryLineInterface.log("Sync to ScheduledJob Process Summary", processSummary, List.of(logPair("sourceTable", tableName)));
}
catch(Exception e)
{
LOG.warn("Error syncing records to scheduled jobs", e, logPair("sourceTable", tableName), logPair("sourceRecordIds", sourceRecordIds));
}
}
/***************************************************************************
* Delete scheduled job records for source-table records that have been deleted.
*
* This method is normally called from within this class, in postDelete.
*
* Note that if the {@link ScheduledJob} table isn't defined in the QInstance,
* that the process will not be called.
*
* @param records list of records to use as foreign-key sources to identify
* scheduledJob records to delete
***************************************************************************/
public void deleteScheduledJobsForRecordList(List<QRecord> records)
{
if(QContext.getQInstance().getTable(ScheduledJob.TABLE_NAME) == null)
{
LOG.info("ScheduledJob table not found, skipping scheduled job delete.");
return;
}
List<String> sourceRecordIds = records.stream()
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
.map(r -> r.getValueString("id")).toList();
if(sourceRecordIds.isEmpty())
{
return;
}
///////////////////////////////////////////////////
// delete any corresponding scheduledJob records //
///////////////////////////////////////////////////
try
{
new DeleteAction().execute(new DeleteInput(ScheduledJob.TABLE_NAME).withQueryFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("foreignKeyType", QCriteriaOperator.EQUALS, getScheduledJobForeignKeyType()))
.withCriteria(new QFilterCriteria("foreignKeyValue", QCriteriaOperator.IN, sourceRecordIds))));
}
catch(Exception e)
{
LOG.warn("Error deleting scheduled jobs for scheduled records", e, logPair("sourceTable", tableName), logPair("sourceRecordIds", sourceRecordIds));
}
}
/*******************************************************************************
** 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 BaseSyncToScheduledJobTableCustomizer withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for syncProcessName
*******************************************************************************/
public String getSyncProcessName()
{
return (this.syncProcessName);
}
/*******************************************************************************
** Setter for syncProcessName
*******************************************************************************/
public void setSyncProcessName(String syncProcessName)
{
this.syncProcessName = syncProcessName;
}
/*******************************************************************************
** Fluent setter for syncProcessName
*******************************************************************************/
public BaseSyncToScheduledJobTableCustomizer withSyncProcessName(String syncProcessName)
{
this.syncProcessName = syncProcessName;
return (this);
}
/*******************************************************************************
** Getter for scheduledJobForeignKeyType
*******************************************************************************/
public String getScheduledJobForeignKeyType()
{
return (this.scheduledJobForeignKeyType);
}
/*******************************************************************************
** Setter for scheduledJobForeignKeyType
*******************************************************************************/
public void setScheduledJobForeignKeyType(String scheduledJobForeignKeyType)
{
this.scheduledJobForeignKeyType = scheduledJobForeignKeyType;
}
/*******************************************************************************
** Fluent setter for scheduledJobForeignKeyType
*******************************************************************************/
public BaseSyncToScheduledJobTableCustomizer withScheduledJobForeignKeyType(String scheduledJobForeignKeyType)
{
this.scheduledJobForeignKeyType = scheduledJobForeignKeyType;
return (this);
}
}

View File

@ -1,76 +0,0 @@
/*
* 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.utils.collections;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Version of map where string keys are handled case-insensitively. e.g.,
** map.put("One", 1); map.get("ONE") == 1.
*******************************************************************************/
public class TypeTolerantKeyMap<V> extends TransformedKeyMap<Serializable, Serializable, V>
{
/***************************************************************************
*
***************************************************************************/
public TypeTolerantKeyMap(QFieldType qFieldType)
{
super(key -> ValueUtils.getValueAsFieldType(qFieldType, key));
}
/***************************************************************************
*
***************************************************************************/
public TypeTolerantKeyMap(QFieldType qFieldType, Supplier<Map<Serializable, V>> supplier)
{
super(key -> ValueUtils.getValueAsFieldType(qFieldType, key), supplier);
}
/***************************************************************************
*
***************************************************************************/
public TypeTolerantKeyMap(Class<? extends Serializable> c)
{
super(key -> ValueUtils.getValueAsType(c, key));
}
/***************************************************************************
*
***************************************************************************/
public TypeTolerantKeyMap(Class<? extends Serializable> c, Supplier<Map<Serializable, V>> supplier)
{
super(key -> ValueUtils.getValueAsType(c, key), supplier);
}
}

View File

@ -1,141 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.customizers;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for MultiCustomizer
*******************************************************************************/
class MultiCustomizerTest extends BaseTest
{
private static List<String> events = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void beforeAndAfterEach()
{
events.clear();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, MultiCustomizer.of(
new QCodeReference(CustomizerA.class),
new QCodeReference(CustomizerB.class)
));
reInitInstanceInContext(qInstance);
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord()));
assertThat(events).hasSize(2)
.contains("CustomizerA.preInsert")
.contains("CustomizerB.preInsert");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAddingMore() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QCodeReferenceWithProperties multiCustomizer = MultiCustomizer.of(new QCodeReference(CustomizerA.class));
MultiCustomizer.addTableCustomizer(multiCustomizer, new QCodeReference(CustomizerB.class));
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).withCustomizer(TableCustomizers.PRE_INSERT_RECORD, multiCustomizer);
reInitInstanceInContext(qInstance);
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord()));
assertThat(events).hasSize(2)
.contains("CustomizerA.preInsert")
.contains("CustomizerB.preInsert");
}
/***************************************************************************
**
***************************************************************************/
public static class CustomizerA implements TableCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
events.add("CustomizerA.preInsert");
return (records);
}
}
/***************************************************************************
**
***************************************************************************/
public static class CustomizerB implements TableCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
events.add("CustomizerB.preInsert");
return (records);
}
}
}

View File

@ -1,70 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.customizers;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for OldRecordHelper
*******************************************************************************/
class OldRecordHelperTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
OldRecordHelper oldRecordHelper = new OldRecordHelper(TestUtils.TABLE_NAME_PERSON_MEMORY, Optional.of(List.of(
new QRecord().withValue("id", 1)
)));
assertTrue(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 1)).isPresent());
assertTrue(oldRecordHelper.getOldRecord(new QRecord().withValue("id", "1")).isPresent());
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 2)).isPresent());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testEmptyOldRecords()
{
OldRecordHelper oldRecordHelper = new OldRecordHelper(TestUtils.TABLE_NAME_PERSON_MEMORY, Optional.empty());
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 1)).isPresent());
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", "1")).isPresent());
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 2)).isPresent());
}
}

View File

@ -1,150 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.tables;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
**
*******************************************************************************/
public class InsertActionInstanceLevelTableCustomizersTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInstanceLevelCustomizers() throws QException
{
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(BreaksEverythingCustomizer.class));
QRecord record = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("name", "octogon"))).getRecords().get(0);
assertEquals("Everything is broken", record.getErrorsAsString());
assertNull(record.getValueInteger("id"));
QContext.getQInstance().setTableCustomizers(new ListingHash<>());
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SetsFirstName.class));
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SetsLastName.class));
QContext.getQInstance().withTableCustomizer(TableCustomizers.POST_INSERT_RECORD, new QCodeReference(DoesNothing.class));
DoesNothing.callCount = 0;
record = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("name", "octogon"))).getRecords().get(0);
assertEquals("Jeff", record.getValueString("firstName"));
assertEquals("Smith", record.getValueString("lastName"));
assertNotNull(record.getValueInteger("id"));
assertEquals(1, DoesNothing.callCount);
}
/*******************************************************************************
**
*******************************************************************************/
public static class BreaksEverythingCustomizer implements TableCustomizerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
records.forEach(r -> r.addError(new SystemErrorStatusMessage("Everything is broken")));
return records;
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class SetsFirstName implements TableCustomizerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
records.forEach(r -> r.setValue("firstName", "Jeff"));
return records;
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class SetsLastName implements TableCustomizerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
records.forEach(r -> r.setValue("lastName", "Smith"));
return records;
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class DoesNothing implements TableCustomizerInterface
{
static int callCount = 0;
/***************************************************************************
*
***************************************************************************/
@Override
public List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
callCount++;
return records;
}
}
}

View File

@ -1,70 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.tables;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
**
*******************************************************************************/
public class UpdateActionInstanceLevelTableCustomizersTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInstanceLevelCustomizers() throws QException
{
QRecord record = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("name", "octogon"))).getRecords().get(0);
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.BreaksEverythingCustomizer.class));
record = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", record.getValue("id")).withValue("name", "octogon"))).getRecords().get(0);
assertEquals("Everything is broken", record.getErrorsAsString());
QContext.getQInstance().setTableCustomizers(new ListingHash<>());
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.SetsFirstName.class));
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.SetsLastName.class));
QContext.getQInstance().withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.DoesNothing.class));
InsertActionInstanceLevelTableCustomizersTest.DoesNothing.callCount = 0;
record = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", record.getValue("id")).withValue("name", "octogon"))).getRecords().get(0);
assertEquals("Jeff", record.getValueString("firstName"));
assertEquals("Smith", record.getValueString("lastName"));
assertNotNull(record.getValueInteger("id"));
assertEquals(1, InsertActionInstanceLevelTableCustomizersTest.DoesNothing.callCount);
}
}

View File

@ -23,22 +23,14 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper.RecordWithErrors;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.AND;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
@ -114,29 +106,4 @@ class ValidateRecordSecurityLockHelperTest extends BaseTest
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAllowedToReadRecord() throws QException
{
QTableMetaData table = QContext.getQInstance().getTables().get(TestUtils.TABLE_NAME_ORDER);
QSession sessionWithStore1 = new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
QSession sessionWithStore2 = new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 2);
QSession sessionWithStore1and2 = new QSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1, 2)));
QSession sessionWithStoresAllAccess = new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
QSession sessionWithNoStores = new QSession();
QRecord recordStore1 = new QRecord().withValue("storeId", 1);
assertTrue(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStore1, null));
assertFalse(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStore2, null));
assertTrue(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStore1and2, null));
assertTrue(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStoresAllAccess, null));
assertFalse(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithNoStores, null));
}
}

View File

@ -176,30 +176,6 @@ public class QInstanceValidatorTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInstanceLevelTableCustomizers()
{
assertValidationFailureReasons((qInstance) -> qInstance.withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(QInstanceValidator.class)),
"Instance tableCustomizer of type preInsertRecord: CodeReference is not of the expected type");
assertValidationFailureReasons((qInstance) ->
{
qInstance.withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(QInstanceValidator.class));
qInstance.withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(QInstanceValidator.class));
qInstance.withTableCustomizer(TableCustomizers.PRE_DELETE_RECORD, new QCodeReference(QInstanceValidator.class));
},
"Instance tableCustomizer of type postUpdateRecord: CodeReference is not of the expected type",
"Instance tableCustomizer of type postUpdateRecord: CodeReference is not of the expected type",
"Instance tableCustomizer of type preDeleteRecord: CodeReference is not of the expected type");
assertValidationSuccess((qInstance) -> qInstance.withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(CustomizerValid.class)));
}
/*******************************************************************************
** Test an instance with null backends - should throw.
**

View File

@ -80,7 +80,7 @@ class TablesCustomPossibleValueProviderTest extends BaseTest
**
*******************************************************************************/
@Test
void testGetPossibleValue() throws QException
void testGetPossibleValue()
{
TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider();

View File

@ -1,174 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.tables;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for QQQTableCustomPossibleValueProvider
*******************************************************************************/
class QQQTableCustomPossibleValueProviderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(new QTableMetaData()
.withName("hidden")
.withIsHidden(true)
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
qInstance.addTable(new QTableMetaData()
.withName("restricted")
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION))
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
new QQQTablesMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
QContext.init(qInstance, newSession());
for(String tableName : qInstance.getTables().keySet())
{
QQQTableTableManager.getQQQTableId(qInstance, tableName);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetPossibleValue() throws QException
{
Integer personTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON);
QQQTableCustomPossibleValueProvider provider = new QQQTableCustomPossibleValueProvider();
QPossibleValue<Integer> possibleValue = provider.getPossibleValue(personTableId);
assertEquals(personTableId, possibleValue.getId());
assertEquals("Person", possibleValue.getLabel());
assertNull(provider.getPossibleValue(-1));
Integer hiddenTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "hidden");
assertNull(provider.getPossibleValue(hiddenTableId));
Integer restrictedTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "restricted");
assertNull(provider.getPossibleValue(restrictedTableId));
QContext.getQSession().withPermission("restricted.hasAccess");
assertNotNull(provider.getPossibleValue(restrictedTableId));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSearchPossibleValue() throws QException
{
Integer personTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON);
Integer shapeTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_SHAPE);
Integer hiddenTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "hidden");
Integer restrictedTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "restricted");
QQQTableCustomPossibleValueProvider provider = new QQQTableCustomPossibleValueProvider();
List<QPossibleValue<Integer>> list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(QQQTable.TABLE_NAME));
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
assertThat(list).noneMatch(p -> p.getId().equals(-1));
assertThat(list).noneMatch(p -> p.getId().equals(hiddenTableId));
assertThat(list).noneMatch(p -> p.getId().equals(restrictedTableId));
assertNull(provider.getPossibleValue("restricted"));
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
.withIdList(List.of(personTableId, shapeTableId, hiddenTableId)));
assertEquals(2, list.size());
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
assertThat(list).anyMatch(p -> p.getId().equals(shapeTableId));
assertThat(list).noneMatch(p -> p.getId().equals(hiddenTableId));
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
.withLabelList(List.of("Person", "Shape", "Restricted")));
assertEquals(2, list.size());
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
assertThat(list).anyMatch(p -> p.getId().equals(shapeTableId));
assertThat(list).noneMatch(p -> p.getId().equals(restrictedTableId));
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
.withSearchTerm("restricted"));
assertEquals(0, list.size());
/////////////////////////////////////////
// add permission for restricted table //
/////////////////////////////////////////
QContext.getQSession().withPermission("restricted.hasAccess");
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
.withSearchTerm("restricted"));
assertEquals(1, list.size());
list = provider.search(new SearchPossibleValueSourceInput()
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
.withLabelList(List.of("Person", "Shape", "Restricted")));
assertEquals(3, list.size());
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
assertThat(list).anyMatch(p -> p.getId().equals(shapeTableId));
assertThat(list).anyMatch(p -> p.getId().equals(restrictedTableId));
}
}

View File

@ -1,193 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.scheduler.processes;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
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.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for AbstractRecordSyncToScheduledJobProcess
*******************************************************************************/
class AbstractRecordSyncToScheduledJobProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
QInstance qInstance = QContext.getQInstance();
new ScheduledJobsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
qInstance.addProcess(new SyncPersonToScheduledJobProcess().produce(qInstance));
qInstance.addPossibleValueSource(new TimeZonePossibleValueSourceMetaDataProvider().produce());
QScheduleManager.initInstance(qInstance, QSystemUserSession::new);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QRecord person = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withRecord(new QRecord().withValue("id", 1701).withValue("firstName", "Darin")))
.getRecords().get(0);
RunProcessInput input = new RunProcessInput();
input.setProcessName(SyncPersonToScheduledJobProcess.class.getSimpleName());
input.setCallback(QProcessCallbackFactory.forRecord(person));
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
new RunProcessAction().execute(input);
List<ScheduledJob> scheduledJobs = new QueryAction().execute(new QueryInput(ScheduledJob.TABLE_NAME).withIncludeAssociations(true)).getRecordEntities(ScheduledJob.class);
assertEquals(1, scheduledJobs.size());
ScheduledJob scheduledJob = scheduledJobs.get(0);
assertEquals(TestUtils.TABLE_NAME_PERSON_MEMORY, scheduledJob.getForeignKeyType());
assertEquals(person.getValueString("id"), scheduledJob.getForeignKeyValue());
assertEquals(60, scheduledJob.getRepeatSeconds());
assertTrue(scheduledJob.getIsActive());
assertEquals(4, scheduledJob.getJobParameters().size());
assertEquals(TestUtils.PROCESS_NAME_GREET_PEOPLE, scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals("processName")).findFirst().get().getValue());
assertEquals("true", scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals("isScheduledJob")).findFirst().get().getValue());
assertEquals(person.getValueString("id"), scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals(TestUtils.TABLE_NAME_PERSON_MEMORY + "Id")).findFirst().get().getValue());
assertEquals(person.getValueString("id"), scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals("recordId")).findFirst().get().getValue());
/////////////////////////////////////////////////////////////////////////////////////////
// re-run - it should update the repeat seconds (per custom logic in test class below) //
/////////////////////////////////////////////////////////////////////////////////////////
new RunProcessAction().execute(input);
scheduledJobs = new QueryAction().execute(new QueryInput(ScheduledJob.TABLE_NAME).withIncludeAssociations(true)).getRecordEntities(ScheduledJob.class);
assertEquals(1, scheduledJobs.size());
scheduledJob = scheduledJobs.get(0);
assertEquals(61, scheduledJob.getRepeatSeconds());
}
/***************************************************************************
**
***************************************************************************/
public static class SyncPersonToScheduledJobProcess extends AbstractRecordSyncToScheduledJobProcess
{
/***************************************************************************
**
***************************************************************************/
@Override
protected ScheduledJob customizeScheduledJob(ScheduledJob scheduledJob, QRecord sourceRecord) throws QException
{
if(scheduledJob.getRepeatSeconds() != null)
{
///////////////////////////////////
// increment by one on an update //
///////////////////////////////////
return scheduledJob.withRepeatSeconds(scheduledJob.getRepeatSeconds() + 1);
}
else
{
return scheduledJob.withRepeatSeconds(60);
}
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getScheduledJobForeignKeyType()
{
return TestUtils.TABLE_NAME_PERSON_MEMORY;
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getRecordForeignKeyFieldName()
{
return "id";
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getRecordForeignKeyPossibleValueSourceName()
{
return TestUtils.TABLE_NAME_PERSON_MEMORY;
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getSourceTableName()
{
return TestUtils.TABLE_NAME_PERSON_MEMORY;
}
/***************************************************************************
**
***************************************************************************/
@Override
protected String getProcessNameScheduledJobParameter()
{
return TestUtils.PROCESS_NAME_GREET_PEOPLE;
}
}
}

View File

@ -1,74 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.scheduler.processes;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for BaseSyncToScheduledJobTableCustomizer
*******************************************************************************/
class BaseSyncToScheduledJobTableCustomizerTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
new AbstractRecordSyncToScheduledJobProcessTest().beforeEach();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
BaseSyncToScheduledJobTableCustomizer.setTableCustomizers(table, new AbstractRecordSyncToScheduledJobProcessTest.SyncPersonToScheduledJobProcess());
QRecord person = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord().withValue("firstName", "Darin"))).getRecords().get(0);
assertEquals(1, QueryAction.execute(ScheduledJob.TABLE_NAME, null).size());
new DeleteAction().execute(new DeleteInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withPrimaryKey(person.getValue("id")));
assertEquals(0, QueryAction.execute(ScheduledJob.TABLE_NAME, null).size());
}
}

View File

@ -1,59 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.utils.collections;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for TypeTolerantKeyMap
*******************************************************************************/
class TypeTolerantKeyMapTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
TypeTolerantKeyMap<QRecord> map = new TypeTolerantKeyMap<>(QFieldType.INTEGER);
map.put(1, new QRecord().withValue("id", 1));
map.put("2", new QRecord().withValue("id", 2));
map.put(3.0, new QRecord().withValue("id", 3));
map.put(new BigDecimal("4.00"), new QRecord().withValue("id", 4));
for(int i=1; i<=4; i++)
{
assertTrue(map.containsKey(i));
assertEquals(i, map.get(i).getValueInteger("id"));
}
}
}

View File

@ -111,7 +111,7 @@ public abstract class AbstractRDBMSAction
**
** That is, table.backendDetails.tableName if set -- else, table.name
*******************************************************************************/
public static String getTableName(QTableMetaData table)
protected String getTableName(QTableMetaData table)
{
if(table.getBackendDetails() instanceof RDBMSTableBackendDetails details)
{
@ -130,7 +130,7 @@ public abstract class AbstractRDBMSAction
**
** That is, field.backendName if set -- else, field.name
*******************************************************************************/
public static String getColumnName(QFieldMetaData field)
protected String getColumnName(QFieldMetaData field)
{
if(field.getBackendName() != null)
{

View File

@ -1,331 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.module.rdbms.model.metadata;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.instances.assessment.QInstanceAssessor;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.rdbms.actions.AbstractRDBMSAction;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
/*******************************************************************************
**
*******************************************************************************/
public class RDBMSBackendAssessor
{
private QInstanceAssessor assessor;
private RDBMSBackendMetaData backendMetaData;
private List<QTableMetaData> tables;
private Map<String, QFieldType> typeMap = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
public RDBMSBackendAssessor(QInstanceAssessor assessor, RDBMSBackendMetaData backendMetaData, List<QTableMetaData> tables)
{
this.assessor = assessor;
this.backendMetaData = backendMetaData;
this.tables = tables;
////////////////////////////////////////////////
// these are types as returned by mysql //
// let null in here mean unsupported QQQ type //
////////////////////////////////////////////////
typeMap.put("TEXT", QFieldType.TEXT);
typeMap.put("BINARY", QFieldType.BLOB);
typeMap.put("SET", null);
typeMap.put("VARBINARY", QFieldType.BLOB);
typeMap.put("MEDIUMBLOB", QFieldType.BLOB);
typeMap.put("NUMERIC", QFieldType.INTEGER);
typeMap.put("BIGINT UNSIGNED", QFieldType.INTEGER);
typeMap.put("MEDIUMINT UNSIGNED", QFieldType.INTEGER);
typeMap.put("SMALLINT UNSIGNED", QFieldType.INTEGER);
typeMap.put("TINYINT UNSIGNED", QFieldType.INTEGER);
typeMap.put("BIT", null);
typeMap.put("FLOAT", null);
typeMap.put("REAL", null);
typeMap.put("VARCHAR", QFieldType.STRING);
typeMap.put("BOOL", QFieldType.BOOLEAN);
typeMap.put("YEAR", null);
typeMap.put("TIME", QFieldType.TIME);
typeMap.put("TIMESTAMP", QFieldType.DATE_TIME);
}
/*******************************************************************************
**
*******************************************************************************/
public void assess()
{
try(Connection connection = new ConnectionManager().getConnection(backendMetaData))
{
////////////////////////////////////////////////////////////////////
// read data type ids (integers) to names, for field-type mapping //
////////////////////////////////////////////////////////////////////
DatabaseMetaData databaseMetaData;
Map<Integer, String> dataTypeMap = new HashMap<>();
try
{
databaseMetaData = connection.getMetaData();
ResultSet typeInfoResultSet = databaseMetaData.getTypeInfo();
while(typeInfoResultSet.next())
{
String name = typeInfoResultSet.getString("TYPE_NAME");
Integer id = typeInfoResultSet.getInt("DATA_TYPE");
dataTypeMap.put(id, name);
}
}
catch(Exception e)
{
assessor.addError("Error loading metaData from RDBMS for backendName: " + backendMetaData.getName() + " - assessment cannot be completed.", e);
return;
}
///////////////////////////////////////
// process each table in the backend //
///////////////////////////////////////
for(QTableMetaData table : tables)
{
String tableName = AbstractRDBMSAction.getTableName(table);
try
{
///////////////////////////////
// check if the table exists //
///////////////////////////////
String databaseName = backendMetaData.getDatabaseName(); // these work for mysql - unclear about other vendors.
String schemaName = null;
try(ResultSet tableResultSet = databaseMetaData.getTables(databaseName, schemaName, tableName, null))
{
if(!tableResultSet.next())
{
assessor.addError("Table: " + table.getName() + " was not found in backend: " + backendMetaData.getName());
assessor.addSuggestion(suggestCreateTable(table));
continue;
}
//////////////////////////////
// read the table's columns //
//////////////////////////////
Map<String, QFieldMetaData> columnMap = new HashMap<>();
String primaryKeyColumnName = null;
try(ResultSet columnsResultSet = databaseMetaData.getColumns(databaseName, schemaName, tableName, null))
{
while(columnsResultSet.next())
{
String columnName = columnsResultSet.getString("COLUMN_NAME");
String columnSize = columnsResultSet.getString("COLUMN_SIZE");
Integer dataTypeId = columnsResultSet.getInt("DATA_TYPE");
String isNullable = columnsResultSet.getString("IS_NULLABLE");
String isAutoIncrement = columnsResultSet.getString("IS_AUTOINCREMENT");
String dataTypeName = dataTypeMap.get(dataTypeId);
QFieldMetaData columnMetaData = new QFieldMetaData(columnName, typeMap.get(dataTypeName));
columnMap.put(columnName, columnMetaData);
if("YES".equals(isAutoIncrement))
{
primaryKeyColumnName = columnName;
}
}
}
/////////////////////////////////
// diff the columns and fields //
/////////////////////////////////
for(QFieldMetaData column : columnMap.values())
{
boolean fieldExists = table.getFields().values().stream().anyMatch(f -> column.getName().equals(AbstractRDBMSAction.getColumnName(f)));
if(!fieldExists)
{
assessor.addWarning("Table: " + table.getName() + " has a column which was not found in the metaData: " + column.getName());
assessor.addSuggestion("// in QTableMetaData.withName(\"" + table.getName() + "\")\n"
+ ".withField(new QFieldMetaData(\"" + column.getName() + "\", QFieldType." + column.getType() + ").withBackendName(\"" + column.getName() + "\")"); // todo - column_name to fieldName
}
}
for(QFieldMetaData field : table.getFields().values())
{
String columnName = AbstractRDBMSAction.getColumnName(field);
boolean columnExists = columnMap.values().stream().anyMatch(c -> c.getName().equals(columnName));
if(!columnExists)
{
assessor.addError("Table: " + table.getName() + " has a field which was not found in the database: " + field.getName());
assessor.addSuggestion("/* For table [" + tableName + "] in backend [" + table.getBackendName() + " (database " + databaseName + ")]: */\n"
+ "ALTER TABLE " + tableName + " ADD " + QInstanceEnricher.inferBackendName(columnName) + " " + getDatabaseTypeForField(table, field) + ";");
}
}
///////////////////////////////////////////////
// read unique constraints from the database //
///////////////////////////////////////////////
Map<String, Set<String>> uniqueIndexMap = new HashMap<>();
try(ResultSet indexInfoResultSet = databaseMetaData.getIndexInfo(databaseName, schemaName, tableName, true, true))
{
while(indexInfoResultSet.next())
{
String indexName = indexInfoResultSet.getString("INDEX_NAME");
String columnName = indexInfoResultSet.getString("COLUMN_NAME");
uniqueIndexMap.computeIfAbsent(indexName, k -> new HashSet<>());
uniqueIndexMap.get(indexName).add(columnName);
}
}
//////////////////////////
// diff the unique keys //
//////////////////////////
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
{
Set<String> fieldNames = uniqueKey.getFieldNames().stream().map(fieldName -> AbstractRDBMSAction.getColumnName(table.getField(fieldName))).collect(Collectors.toSet());
if(!uniqueIndexMap.containsValue(fieldNames))
{
assessor.addWarning("Table: " + table.getName() + " specifies a uniqueKey which was not found in the database: " + uniqueKey.getFieldNames());
assessor.addSuggestion("/* For table [" + tableName + "] in backend [" + table.getBackendName() + " (database " + databaseName + ")]: */\n"
+ "ALTER TABLE " + tableName + " ADD UNIQUE (" + StringUtils.join(", ", fieldNames) + ");");
}
}
for(Set<String> uniqueIndex : uniqueIndexMap.values())
{
//////////////////////////
// skip the primary key //
//////////////////////////
if(uniqueIndex.size() == 1 && uniqueIndex.contains(primaryKeyColumnName))
{
continue;
}
boolean foundInTableMetaData = false;
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
{
Set<String> fieldNames = uniqueKey.getFieldNames().stream().map(fieldName -> AbstractRDBMSAction.getColumnName(table.getField(fieldName))).collect(Collectors.toSet());
if(uniqueIndex.equals(fieldNames))
{
foundInTableMetaData = true;
break;
}
}
if(!foundInTableMetaData)
{
assessor.addWarning("Table: " + table.getName() + " has a unique index which was not found in the metaData: " + uniqueIndex);
assessor.addSuggestion("// in QTableMetaData.withName(\"" + table.getName() + "\")\n"
+ ".withUniqueKey(new UniqueKey(\"" + StringUtils.join("\", \"", uniqueIndex) + "\"))");
}
}
}
}
catch(Exception e)
{
assessor.addError("Error assessing table: " + table.getName() + " in backend: " + backendMetaData.getName(), e);
}
}
}
catch(Exception e)
{
assessor.addError("Error connecting to RDBMS for backendName: " + backendMetaData.getName(), e);
return;
}
}
/*******************************************************************************
**
*******************************************************************************/
private String suggestCreateTable(QTableMetaData table)
{
StringBuilder rs = new StringBuilder("/* For table [" + table.getName() + "] in backend [" + table.getBackendName() + " (database " + (backendMetaData.getDatabaseName()) + ")]: */\n");
rs.append("CREATE TABLE ").append(AbstractRDBMSAction.getTableName(table)).append("\n");
rs.append("(\n");
List<String> fields = new ArrayList<>();
for(QFieldMetaData field : table.getFields().values())
{
fields.add(" " + AbstractRDBMSAction.getColumnName(field) + " " + getDatabaseTypeForField(table, field));
}
rs.append(StringUtils.join(",\n", fields));
rs.append("\n);");
return (rs.toString());
}
/*******************************************************************************
**
*******************************************************************************/
private String getDatabaseTypeForField(QTableMetaData table, QFieldMetaData field)
{
return switch(field.getType())
{
case STRING ->
{
int n = Objects.requireNonNullElse(field.getMaxLength(), 250);
yield ("VARCHAR(" + n + ")");
}
case INTEGER ->
{
String suffix = table.getPrimaryKeyField().equals(field.getName()) ? " AUTO_INCREMENT PRIMARY KEY" : "";
yield ("INTEGER" + suffix);
}
case LONG ->
{
String suffix = table.getPrimaryKeyField().equals(field.getName()) ? " AUTO_INCREMENT PRIMARY KEY" : "";
yield ("BIGINT" + suffix);
}
case DECIMAL -> "DECIMAL(10,2)";
case BOOLEAN -> "BOOLEAN";
case DATE -> "DATE";
case TIME -> "TIME";
case DATE_TIME -> "TIMESTAMP";
case TEXT -> "TEXT";
case HTML -> "TEXT";
case PASSWORD -> "VARCHAR(40)";
case BLOB -> "BLOB";
};
}
}

View File

@ -22,18 +22,12 @@
package com.kingsrook.qqq.backend.module.rdbms.model.metadata;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.instances.assessment.Assessable;
import com.kingsrook.qqq.backend.core.instances.assessment.QInstanceAssessor;
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.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule;
import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy;
import com.kingsrook.qqq.backend.module.rdbms.strategy.RDBMSActionStrategyInterface;
@ -42,7 +36,7 @@ import com.kingsrook.qqq.backend.module.rdbms.strategy.RDBMSActionStrategyInterf
/*******************************************************************************
** Meta-data to provide details of an RDBMS backend (e.g., connection params)
*******************************************************************************/
public class RDBMSBackendMetaData extends QBackendMetaData implements Assessable
public class RDBMSBackendMetaData extends QBackendMetaData
{
private String vendor;
private String hostName;
@ -586,25 +580,4 @@ public class RDBMSBackendMetaData extends QBackendMetaData implements Assessable
}
/***************************************************************************
**
***************************************************************************/
@Override
public void assess(QInstanceAssessor qInstanceAssessor, QInstance qInstance)
{
List<QTableMetaData> tables = new ArrayList<>();
for(QTableMetaData table : qInstance.getTables().values())
{
if(Objects.equals(getName(), table.getBackendName()))
{
tables.add(table);
}
}
if(!tables.isEmpty())
{
new RDBMSBackendAssessor(qInstanceAssessor, this, tables).assess();
}
}
}

View File

@ -101,6 +101,12 @@ public class RDBMSTableMetaDataBuilder
String schemaName = null;
String tableNameForMetaDataQueries = tableName;
if(backendMetaData.getVendor().equals("h2"))
{
databaseName = databaseName.toUpperCase();
tableNameForMetaDataQueries = tableName.toUpperCase();
}
try(ResultSet tableResultSet = databaseMetaData.getTables(databaseName, schemaName, tableNameForMetaDataQueries, null))
{
if(!tableResultSet.next())

View File

@ -153,22 +153,12 @@ public class TestUtils
*******************************************************************************/
public static RDBMSBackendMetaData defineBackend()
{
RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData()
return (new RDBMSBackendMetaData()
.withName(DEFAULT_BACKEND_NAME)
.withVendor("h2")
.withHostName("mem")
.withDatabaseName("test_database")
.withUsername("sa");
////////////////////////////////////////////////////////////////////
// by default h2 up-shifts all names, which isn't how we expected //
// things to be, so, tell it not to do that. //
////////////////////////////////////////////////////////////////////
String jdbcUrl = ConnectionManager.getJdbcUrl(rdbmsBackendMetaData);
jdbcUrl += ";DATABASE_TO_UPPER=FALSE";
rdbmsBackendMetaData.setJdbcUrl(jdbcUrl);
return rdbmsBackendMetaData;
.withUsername("sa"));
}

View File

@ -438,8 +438,8 @@ class QueryManagerTest extends BaseTest
""");
List<Map<String, Object>> rows = QueryManager.executeStatementForRows(connection, "SELECT * FROM test_table");
assertNotNull(rows);
assertEquals(47, rows.get(0).get("int_col"));
assertEquals("Q", rows.get(0).get("char_col"));
assertEquals(47, rows.get(0).get("INT_COL"));
assertEquals("Q", rows.get(0).get("CHAR_COL"));
}
}

View File

@ -1,117 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.module.rdbms.model.metadata;
import java.sql.Connection;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.instances.assessment.QInstanceAssessor;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.module.rdbms.BaseTest;
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/*******************************************************************************
** Unit test for RDBMSBackendAssessor
*******************************************************************************/
class RDBMSBackendAssessorTest extends BaseTest
{
private static final QLogger LOG = QLogger.getLogger(RDBMSBackendAssessorTest.class);
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSuccess() throws Exception
{
TestUtils.primeTestDatabase("prime-test-database.sql");
QInstanceAssessor assessor = new QInstanceAssessor(QContext.getQInstance());
assessor.assess();
System.out.println(assessor.getSummary());
assertEquals(0, assessor.getErrors().size());
assertEquals(0, assessor.getWarnings().size());
assertEquals(0, assessor.getExitCode());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableIssues() throws Exception
{
///////////////////////////////////////////////////////////////////////////////
// start from primed database, but make a few alters to it and the meta-data //
///////////////////////////////////////////////////////////////////////////////
TestUtils.primeTestDatabase("prime-test-database.sql");
ConnectionManager connectionManager = new ConnectionManager();
try(Connection connection = connectionManager.getConnection(TestUtils.defineBackend()))
{
QueryManager.executeUpdate(connection, "ALTER TABLE person ADD COLUMN suffix VARCHAR(20)");
QueryManager.executeUpdate(connection, "ALTER TABLE person ADD UNIQUE u_name (first_name, last_name)");
}
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON)
.withField(new QFieldMetaData("middleName", QFieldType.STRING))
.withUniqueKey(new UniqueKey("firstName", "middleName", "lastName"));
///////////////////////////
// un-prime the database //
///////////////////////////
QInstanceAssessor assessor = new QInstanceAssessor(QContext.getQInstance());
assessor.assess();
LOG.info(assessor.getSummary());
assertNotEquals(0, assessor.getErrors().size());
assertNotEquals(0, assessor.getExitCode());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTotalFailure() throws Exception
{
///////////////////////////
// un-prime the database //
///////////////////////////
TestUtils.primeTestDatabase("drop-test-database.sql");
QInstanceAssessor assessor = new QInstanceAssessor(QContext.getQInstance());
assessor.assess();
System.out.println(assessor.getSummary());
assertNotEquals(0, assessor.getErrors().size());
assertNotEquals(0, assessor.getExitCode());
}
}

View File

@ -122,7 +122,6 @@ public class SharingMetaDataProvider
qInstance.addTable(new QTableMetaData()
.withName(User.TABLE_NAME)
.withPrimaryKeyField("id")
.withBackendDetails(new RDBMSTableBackendDetails().withTableName("user"))
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withFieldsFromEntity(User.class)
.withRecordSecurityLock(new RecordSecurityLock()
@ -133,7 +132,6 @@ public class SharingMetaDataProvider
qInstance.addTable(new QTableMetaData()
.withName(Group.TABLE_NAME)
.withPrimaryKeyField("id")
.withBackendDetails(new RDBMSTableBackendDetails().withTableName("group"))
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withFieldsFromEntity(Group.class));
QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(Group.TABLE_NAME));
@ -141,7 +139,6 @@ public class SharingMetaDataProvider
qInstance.addTable(new QTableMetaData()
.withName(Client.TABLE_NAME)
.withPrimaryKeyField("id")
.withBackendDetails(new RDBMSTableBackendDetails().withTableName("client"))
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withFieldsFromEntity(Client.class));
QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(Client.TABLE_NAME));

View File

@ -1,32 +0,0 @@
--
-- 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/>.
--
DROP TABLE IF EXISTS person;
DROP TABLE IF EXISTS personal_id_card;
DROP TABLE IF EXISTS carrier;
DROP TABLE IF EXISTS line_item_extrinsic;
DROP TABLE IF EXISTS order_line;
DROP TABLE IF EXISTS item;
DROP TABLE IF EXISTS `order`;
DROP TABLE IF EXISTS order_instructions;
DROP TABLE IF EXISTS warehouse_store_int;
DROP TABLE IF EXISTS store;
DROP TABLE IF EXISTS warehouse;

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-dev-tools</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>QQQ Dev Tools</name>
<description>Tools for developers of QQQ (is that the framework or applications or qbits or what?)</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.0</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,342 +0,0 @@
package com.kingsrook.qqq.devtools;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Consumer;
/*******************************************************************************
** todo picocli this project and class
*******************************************************************************/
public class CreateNewQBit
{
private String name;
private String root;
private static ExecutorService executorService = null;
private static String SED = "/opt/homebrew/bin/gsed"; // needs to be a version that supports -i (in-place edit)
private static String GIT = "/usr/bin/git";
private static String CP = "/bin/cp";
private static String MV = "/bin/mv";
/*******************************************************************************
**
*******************************************************************************/
public static void main(String[] args)
{
args = new String[] { "/Users/dkelkhoff/git/kingsrook/qbits", "webhooks" };
if(args.length < 2)
{
System.out.println("Usage: java CreateNewQBit root-dir qbit-name");
System.exit(1);
}
CreateNewQBit instance = new CreateNewQBit();
instance.root = args[0];
instance.name = args[1];
System.exit(instance.run());
}
/*******************************************************************************
**
*******************************************************************************/
public int run()
{
try
{
String wordsName = makeWordsName(name);
wordsName = stripQBitPrefix(wordsName);
String dashName = makeDashName(wordsName);
String packageName = makePackageName(wordsName);
String className = makeClassName(wordsName);
String varName = makeVarName(wordsName);
if(!new File(root).exists())
{
System.err.println("ERROR: Root directory [" + root + "] does not exist.");
return (1);
}
File template = new File(root + File.separator + "TEMPLATE");
if(!template.exists())
{
System.err.println("ERROR: Template directory [TEMPLATE] does not exist under [" + root + "].");
return (1);
}
File dir = new File(root + File.separator + "qbit-" + dashName);
if(dir.exists())
{
System.err.println("ERROR: Directory [" + dashName + "] already exists under [" + root + "].");
return (1);
}
System.out.println("Creating qbit-" + dashName + ":");
System.out.printf("%13s %s\n", "packgaename:", packageName);
System.out.printf("%13s %s\n", "ClassName:", className);
System.out.printf("%13s %s\n", "varName:", varName);
System.out.println();
System.out.println("Copying template...");
ProcessResult cpResult = run(new ProcessBuilder(CP, "-rv", template.getAbsolutePath(), dir.getAbsolutePath()));
System.out.print(cpResult.stdout());
System.out.println();
System.out.println("Renaming files...");
renameFiles(dir, packageName, className);
System.out.println();
System.out.println("Updating file contents...");
replacePlaceholders(dir, dashName, packageName, className, varName);
System.out.println();
System.out.println("Init'ing git repo...");
run(new ProcessBuilder(GIT, "init").directory(dir));
System.out.println();
// git remote add origin https://github.com/Kingsrook/${name}.git ?
// echo https://app.circleci.com/projects/project-dashboard/github/Kingsrook after initial push
}
catch(Exception e)
{
e.printStackTrace();
return 1;
}
return 0;
}
/***************************************************************************
**
***************************************************************************/
void renameFiles(File dir, String packageName, String className) throws Exception
{
String srcPath = dir.getAbsolutePath() + "/src/main/java/com/kingsrook/qbits";
String packagePath = packageName.replace('.', '/');
System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo/TodoQBitConfig.java", srcPath + "/todo/" + className + "QBitConfig.java")).stdout());
System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo/TodoQBitProducer.java", srcPath + "/todo/" + className + "QBitProducer.java")).stdout());
System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo", srcPath + "/" + packagePath)).stdout());
}
/***************************************************************************
**
***************************************************************************/
static void replacePlaceholders(File dir, String dashName, String packageName, String className, String varName) throws Exception
{
for(File file : dir.listFiles())
{
if(file.isDirectory())
{
replacePlaceholders(file, dashName, packageName, className, varName);
continue;
}
System.out.println("Replacing placeholders in: " + file.getAbsolutePath());
replaceOne("dashName", dashName, file);
replaceOne("packageName", packageName, file);
replaceOne("className", className, file);
replaceOne("varName", varName, file);
}
}
/***************************************************************************
**
***************************************************************************/
static void replaceOne(String from, String to, File file) throws Exception
{
run(new ProcessBuilder(SED, "s/\\${" + from + "}/" + to + "/g", "-i", file.getAbsolutePath()));
}
/***************************************************************************
**
***************************************************************************/
public record ProcessResult(Integer exitCode, String stdout, String stderr)
{
/***************************************************************************
*
***************************************************************************/
public boolean hasStdout()
{
return stdout != null && !stdout.isEmpty();
}
/***************************************************************************
*
***************************************************************************/
public boolean hasStderr()
{
return stderr != null && !stderr.isEmpty();
}
}
/***************************************************************************
**
***************************************************************************/
public static ProcessResult run(ProcessBuilder builder) throws Exception
{
StringBuilder stdout = new StringBuilder();
StringBuilder stderr = new StringBuilder();
Process process = builder.start();
Future<?> stdoutFuture = getExecutorService().submit(new StreamGobbler(process.getInputStream(), stdout::append));
Future<?> stderrFuture = getExecutorService().submit(new StreamGobbler(process.getErrorStream(), stderr::append));
int exitCode = process.waitFor();
stdoutFuture.get();
stderrFuture.get();
return (new ProcessResult(exitCode, stdout.toString(), stderr.toString()));
}
/***************************************************************************
**
***************************************************************************/
private static class StreamGobbler implements Runnable
{
private InputStream inputStream;
private Consumer<String> consumer;
/***************************************************************************
**
***************************************************************************/
public StreamGobbler(InputStream inputStream, Consumer<String> consumer)
{
this.inputStream = inputStream;
this.consumer = consumer;
}
/***************************************************************************
**
***************************************************************************/
@Override
public void run()
{
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(s -> consumer.accept(s + System.lineSeparator()));
}
}
/***************************************************************************
**
***************************************************************************/
private static ExecutorService getExecutorService()
{
if(executorService == null)
{
executorService = Executors.newCachedThreadPool();
}
return (executorService);
}
/***************************************************************************
**
***************************************************************************/
private String makeWordsName(String s)
{
if(s.contains("-"))
{
return (s.toLowerCase().replace('-', ' '));
}
if(s.matches(".*[A-Z].*"))
{
return s.replaceAll("([A-Z])", "$1'").toLowerCase().trim();
}
return s;
}
/***************************************************************************
**
***************************************************************************/
static String stripQBitPrefix(String s)
{
return (s.replaceFirst("^qbit(s) ", ""));
}
/***************************************************************************
**
***************************************************************************/
static String makeDashName(String s)
{
return (s.replace(' ', '-'));
}
/***************************************************************************
**
***************************************************************************/
static String makePackageName(String s)
{
return (s.replace(" ", ""));
}
/***************************************************************************
**
***************************************************************************/
static String makeClassName(String s)
{
StringBuilder rs = new StringBuilder();
String[] words = s.split(" ");
for(String word : words)
{
rs.append(word.substring(0, 1).toUpperCase());
if(word.length() > 1)
{
rs.append(word.substring(1));
}
}
return rs.toString();
}
/***************************************************************************
**
***************************************************************************/
static String makeVarName(String s)
{
String className = makeClassName(s);
return className.substring(0, 1).toLowerCase() + (className.length() == 1 ? "" : className.substring(1));
}
}

View File

@ -56,9 +56,6 @@ public class ApiInstanceMetaDataProvider
public static final String TABLE_NAME_API_LOG = "apiLog";
public static final String TABLE_NAME_API_LOG_USER = "apiLogUser";
public static final String API_NAME_PVS_NAME = "apiName";
public static final String API_VERSION_PVS_NAME = "apiVersion";
/*******************************************************************************
@ -145,7 +142,7 @@ public class ApiInstanceMetaDataProvider
}
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(API_NAME_PVS_NAME)
.withName("apiName")
.withType(QPossibleValueSourceType.ENUM)
.withEnumValues(apiNamePossibleValues));
@ -155,7 +152,7 @@ public class ApiInstanceMetaDataProvider
}
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(API_VERSION_PVS_NAME)
.withName("apiVersion")
.withType(QPossibleValueSourceType.ENUM)
.withEnumValues(apiVersionPossibleValues));
}

View File

@ -36,7 +36,7 @@
<!-- When updating to javalin 6.3.0, we received classNotFound errors - which this fixed. -->
<kotlin.version>1.9.10</kotlin.version>
<javalin.version>6.3.0</javalin.version>
<javalin.version>6.6.0</javalin.version>
</properties>
<dependencies>
@ -117,11 +117,29 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-api</artifactId>
<version>0.26.0-integration-20250615-161253</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>
${project.basedir}/src/test/resources/static-site.jar
</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>

View File

@ -25,10 +25,8 @@ package com.kingsrook.qqq.backend.javalin;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import org.apache.logging.log4j.Level;
@ -331,16 +329,4 @@ public class QJavalinMetaData implements QSupplementalInstanceMetaData
}
/***************************************************************************
**
***************************************************************************/
@Override
public void validate(QInstance qInstance, QInstanceValidator validator)
{
for(JavalinRouteProviderMetaData routeProviderMetaData : CollectionUtils.nonNullList(routeProviders))
{
routeProviderMetaData.validate(qInstance, validator);
}
}
}

View File

@ -26,6 +26,7 @@ import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.api.javalin.QJavalinApiHandler;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
@ -74,9 +75,11 @@ public class QApplicationJavalinServer
private Integer port = 8000;
private boolean serveFrontendMaterialDashboard = true;
private String frontendMaterialDashboardHostedPath = "/"; // TODO - Things like this should be moved into a central configuration file system, so that it can be changed in userspace without code changes.
private boolean serveLegacyUnversionedMiddlewareAPI = true;
private List<AbstractMiddlewareVersion> middlewareVersionList = List.of(new MiddlewareVersionV1());
private List<QJavalinRouteProviderInterface> additionalRouteProviders = null;
private boolean serveApplicationApi = true;
private List<AbstractMiddlewareVersion> middlewareVersionList = List.of(new MiddlewareVersionV1()); // TODO - Seems like this should be null by default, and only set if the application developer wants to serve versioned middleware APIs. @DK
private List<QJavalinRouteProvider> additionalRouteProviders = null;
private Consumer<Javalin> javalinConfigurationCustomizer = null;
private QJavalinMetaData javalinMetaData = null;
@ -132,20 +135,24 @@ public class QApplicationJavalinServer
{
if(resource != null)
{
config.staticFiles.add("/material-dashboard-overlay");
config.staticFiles.add(staticFileConfig ->
{
staticFileConfig.hostedPath = this.frontendMaterialDashboardHostedPath;
staticFileConfig.directory = "/material-dashboard-overlay";
});
}
}
////////////////////////////////////////////////////////////////////////////////////
// tell javalin where to find material-dashboard static web assets //
// in this case, this path is coming from the qqq-frontend-material-dashboard jar //
////////////////////////////////////////////////////////////////////////////////////
config.staticFiles.add("/material-dashboard");
config.staticFiles.add(staticFileConfig ->
{
staticFileConfig.hostedPath = this.frontendMaterialDashboardHostedPath;
staticFileConfig.directory = "/material-dashboard";
});
////////////////////////////////////////////////////////////
// set the index page for the SPA from material dashboard //
////////////////////////////////////////////////////////////
config.spaRoot.addFile("/", "material-dashboard/index.html");
config.spaRoot.addFile(this.frontendMaterialDashboardHostedPath, "material-dashboard/index.html");
}
///////////////////////////////////////////
@ -167,6 +174,20 @@ public class QApplicationJavalinServer
}
}
if(serveApplicationApi)
{
try
{
QJavalinApiHandler qJavalinApiHandler = new QJavalinApiHandler(qInstance);
config.router.apiBuilder(qJavalinApiHandler.getRoutes());
}
catch(Exception e)
{
LOG.error("Unable to add application API routes to Javalin service.", e);
throw new RuntimeException(e);
}
}
/////////////////////////////////////
// versioned qqq middleware routes //
/////////////////////////////////////
@ -183,7 +204,7 @@ public class QApplicationJavalinServer
////////////////////////////////////////////////////////////////////////////
// additional route providers (e.g., application-apis, other middlewares) //
////////////////////////////////////////////////////////////////////////////
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
for(QJavalinRouteProvider routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.setQInstance(qInstance);
@ -201,7 +222,7 @@ public class QApplicationJavalinServer
// also pass the javalin service into any additionalRouteProviders, //
// in case they need additional setup, e.g., before/after handlers. //
//////////////////////////////////////////////////////////////////////
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
for(QJavalinRouteProvider routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.acceptJavalinService(service);
}
@ -379,7 +400,7 @@ public class QApplicationJavalinServer
}
}
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
for(QJavalinRouteProvider routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.setQInstance(newQInstance);
}
@ -470,6 +491,25 @@ public class QApplicationJavalinServer
/*******************************************************************************
* Sets the hosted path for the frontend Material Dashboard UI.
*
* This value determines the base URL path under which the static frontend
* dashboard assets are served. It should match the path configured in your
* frontend build or static asset router.
*
* @param frontendMaterialDashboardHostedPath the hosted path (e.g., "/admin" or "/dashboard"). Default is "/"
*
* @see #withServeFrontendMaterialDashboard(boolean)
*******************************************************************************/
public QApplicationJavalinServer withFrontendMaterialDashboardHostedPath(String frontendMaterialDashboardHostedPath)
{
this.frontendMaterialDashboardHostedPath = frontendMaterialDashboardHostedPath;
return (this);
}
/*******************************************************************************
** Getter for serveLegacyUnversionedMiddlewareAPI
*******************************************************************************/
@ -535,7 +575,7 @@ public class QApplicationJavalinServer
/*******************************************************************************
** Getter for additionalRouteProviders
*******************************************************************************/
public List<QJavalinRouteProviderInterface> getAdditionalRouteProviders()
public List<QJavalinRouteProvider> getAdditionalRouteProviders()
{
return (this.additionalRouteProviders);
}
@ -545,7 +585,7 @@ public class QApplicationJavalinServer
/*******************************************************************************
** Setter for additionalRouteProviders
*******************************************************************************/
public void setAdditionalRouteProviders(List<QJavalinRouteProviderInterface> additionalRouteProviders)
public void setAdditionalRouteProviders(List<QJavalinRouteProvider> additionalRouteProviders)
{
this.additionalRouteProviders = additionalRouteProviders;
}
@ -555,7 +595,7 @@ public class QApplicationJavalinServer
/*******************************************************************************
** Fluent setter for additionalRouteProviders
*******************************************************************************/
public QApplicationJavalinServer withAdditionalRouteProviders(List<QJavalinRouteProviderInterface> additionalRouteProviders)
public QApplicationJavalinServer withAdditionalRouteProviders(List<QJavalinRouteProvider> additionalRouteProviders)
{
this.additionalRouteProviders = additionalRouteProviders;
return (this);
@ -566,7 +606,7 @@ public class QApplicationJavalinServer
/*******************************************************************************
** Fluent setter to add a single additionalRouteProvider
*******************************************************************************/
public QApplicationJavalinServer withAdditionalRouteProvider(QJavalinRouteProviderInterface additionalRouteProvider)
public QApplicationJavalinServer withAdditionalRouteProvider(QJavalinRouteProvider additionalRouteProvider)
{
if(this.additionalRouteProviders == null)
{
@ -690,4 +730,57 @@ public class QApplicationJavalinServer
return (this);
}
/*******************************************************************************
** Getter for frontendMaterialDashboardHostedPath
*
* @see #withFrontendMaterialDashboardHostedPath(String)
*******************************************************************************/
public String getFrontendMaterialDashboardHostedPath()
{
return (this.frontendMaterialDashboardHostedPath);
}
/*******************************************************************************
** Setter for frontendMaterialDashboardHostedPath
*
* @see #withFrontendMaterialDashboardHostedPath(String)
*******************************************************************************/
public void setFrontendMaterialDashboardHostedPath(String frontendMaterialDashboardHostedPath)
{
this.frontendMaterialDashboardHostedPath = frontendMaterialDashboardHostedPath;
}
/*******************************************************************************
** Getter for serveApplicationApi
*******************************************************************************/
public boolean getServeApplicationApi()
{
return (this.serveApplicationApi);
}
/*******************************************************************************
** Setter for serveApplicationApi
*******************************************************************************/
public void setServeApplicationApi(boolean serveApplicationApi)
{
this.serveApplicationApi = serveApplicationApi;
}
/*******************************************************************************
** Fluent setter for serveApplicationApi
*******************************************************************************/
public QApplicationJavalinServer withServeApplicationApi(boolean serveApplicationApi)
{
this.serveApplicationApi = serveApplicationApi;
return (this);
}
}

View File

@ -32,19 +32,20 @@ import io.javalin.config.JavalinConfig;
** Interface for classes that can provide a list of endpoints to a javalin
** server.
*******************************************************************************/
public interface QJavalinRouteProviderInterface
public abstract class QJavalinRouteProvider
{
/***************************************************************************
** For initial setup when server boots, set the qInstance - but also,
** e.g., for development, to do a hot-swap.
***************************************************************************/
void setQInstance(QInstance qInstance);
public abstract void setQInstance(QInstance qInstance);
/***************************************************************************
**
***************************************************************************/
default EndpointGroup getJavalinEndpointGroup()
public EndpointGroup getJavalinEndpointGroup()
{
/////////////////////////////
// no endpoints at default //
@ -58,7 +59,7 @@ public interface QJavalinRouteProviderInterface
** accept the javalinConfig object, to perform whatever setup you need,
** such as setting up routes.
***************************************************************************/
default void acceptJavalinConfig(JavalinConfig config)
public void acceptJavalinConfig(JavalinConfig config)
{
/////////////////////
// noop at default //
@ -70,11 +71,10 @@ public interface QJavalinRouteProviderInterface
** accept the Javalin service object, to perform whatever setup you need,
** such as setting up before/after handlers.
***************************************************************************/
default void acceptJavalinService(Javalin service)
public void acceptJavalinService(Javalin service)
{
/////////////////////
// noop at default //
/////////////////////
}
}

View File

@ -23,13 +23,8 @@ package com.kingsrook.qqq.middleware.javalin.metadata;
import java.util.List;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.RouteProviderContextHandlerInterface;
/*******************************************************************************
@ -37,16 +32,13 @@ import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.Route
*******************************************************************************/
public class JavalinRouteProviderMetaData implements QMetaDataObject
{
private String name;
private String hostedPath;
private String fileSystemPath;
private String processName;
private List<String> methods;
private String hostedPath;
private String spaRootPath;
private String spaRootFile;
private String fileSystemPath;
private String processName;
private List<String> methods;
private QCodeReference routeAuthenticator;
private QCodeReference contextHandler;
@ -216,87 +208,67 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject
/*******************************************************************************
** Getter for contextHandler
** Getter for spaRootPath
*******************************************************************************/
public QCodeReference getContextHandler()
public String getSpaRootPath()
{
return (this.contextHandler);
return (this.spaRootPath);
}
/*******************************************************************************
** Setter for contextHandler
** Setter for spaRootPath
*******************************************************************************/
public void setContextHandler(QCodeReference contextHandler)
public void setSpaRootPath(String spaRootPath)
{
this.contextHandler = contextHandler;
this.spaRootPath = spaRootPath;
}
/*******************************************************************************
** Fluent setter for contextHandler
** Fluent setter for spaRootPath
*******************************************************************************/
public JavalinRouteProviderMetaData withContextHandler(QCodeReference contextHandler)
public JavalinRouteProviderMetaData withSpaRootPath(String spaRootPath)
{
this.contextHandler = contextHandler;
this.spaRootPath = spaRootPath;
return (this);
}
/*******************************************************************************
** Getter for name
* Getter for spaRootFile
* @see #withSpaRootFile(String)
*******************************************************************************/
public String getName()
public String getSpaRootFile()
{
return (this.name);
return (this.spaRootFile);
}
/*******************************************************************************
** Setter for name
* Setter for spaRootFile
* @see #withSpaRootFile(String)
*******************************************************************************/
public void setName(String name)
public void setSpaRootFile(String spaRootFile)
{
this.name = name;
this.spaRootFile = spaRootFile;
}
/*******************************************************************************
** Fluent setter for name
* Fluent setter for spaRootFile
* @param spaRootFile TODO document this property
* @return this
*******************************************************************************/
public JavalinRouteProviderMetaData withName(String name)
public JavalinRouteProviderMetaData withSpaRootFile(String spaRootFile)
{
this.name = name;
this.spaRootFile = spaRootFile;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public void validate(QInstance qInstance, QInstanceValidator validator)
{
String prefix = "In javalinRouteProvider '" + name + "', ";
if(StringUtils.hasContent(processName))
{
validator.assertCondition(qInstance.getProcesses().containsKey(processName), prefix + "unrecognized process name: " + processName + " in a javalinRouteProvider");
}
if(routeAuthenticator != null)
{
validator.validateSimpleCodeReference(prefix + "routeAuthenticator ", routeAuthenticator, RouteAuthenticatorInterface.class);
}
if(contextHandler != null)
{
validator.validateSimpleCodeReference(prefix + "contextHandler ", contextHandler, RouteProviderContextHandlerInterface.class);
}
}
}

View File

@ -22,34 +22,41 @@
package com.kingsrook.qqq.middleware.javalin.routeproviders;
import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.actions.tables.storage.StorageInput;
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.session.QSystemUserSession;
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 com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProvider;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.DefaultRouteProviderContextHandler;
import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.RouteProviderContextHandlerInterface;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessBasedRouter implements QJavalinRouteProviderInterface
public class ProcessBasedRouter extends QJavalinRouteProvider
{
private static final QLogger LOG = QLogger.getLogger(ProcessBasedRouter.class);
@ -58,7 +65,6 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
private final List<String> methods;
private QCodeReference routeAuthenticator;
private QCodeReference contextHandler;
private QInstance qInstance;
@ -82,7 +88,6 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
{
this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods());
setRouteAuthenticator(routeProvider.getRouteAuthenticator());
setContextHandler(routeProvider.getContextHandler());
}
@ -183,27 +188,72 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
{
LOG.info("Running process to serve route", logPair("processName", processName), logPair("path", context.path()));
//////////////////////////////////////////////////////////////////////////////////////
// handle request (either using route's specific context handler, or a default one) //
//////////////////////////////////////////////////////////////////////////////////////
RouteProviderContextHandlerInterface contextHandler = createContextHandler();
contextHandler.handleRequest(context, input);
// todo - make the inputStream available to the process to stream results?
// maybe via the callback object??? input.setCallback(new QProcessCallback() {});
// context.resultInputStream();
/////////////////////
// run the process //
/////////////////////
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
input.addValue("path", context.path());
input.addValue("method", context.method());
input.addValue("pathParams", new HashMap<>(context.pathParamMap()));
input.addValue("queryParams", new HashMap<>(context.queryParamMap()));
input.addValue("formParams", new HashMap<>(context.formParamMap()));
input.addValue("cookies", new HashMap<>(context.cookieMap()));
input.addValue("requestHeaders", new HashMap<>(context.headerMap()));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
/////////////////////
// handle response //
/////////////////////
if(contextHandler.handleResponse(context, runProcessOutput))
/////////////////
// headers map //
/////////////////
Serializable headers = runProcessOutput.getValue("responseHeaders");
if(headers instanceof Map headersMap)
{
for(Object key : headersMap.keySet())
{
context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key)));
}
}
// todo - make the inputStream available to the process
// maybe via the callback object??? input.setCallback(new QProcessCallback() {});
// context.resultInputStream();
//////////////
// response //
//////////////
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
String redirectURL = runProcessOutput.getValueString("redirectURL");
String responseString = runProcessOutput.getValueString("responseString");
byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes");
StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput");
if(StringUtils.hasContent(redirectURL))
{
context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode));
return;
}
if(statusCode != null)
{
context.status(statusCode);
}
if(StringUtils.hasContent(responseString))
{
context.result(responseString);
return;
}
if(responseBytes != null && responseBytes.length > 0)
{
context.result(responseBytes);
return;
}
if(responseStorageInput != null)
{
InputStream inputStream = new StorageAction().getInputStream(responseStorageInput);
context.result(inputStream);
return;
}
@ -221,23 +271,6 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
/***************************************************************************
**
***************************************************************************/
private RouteProviderContextHandlerInterface createContextHandler()
{
if(contextHandler != null)
{
return QCodeLoader.getAdHoc(RouteProviderContextHandlerInterface.class, this.contextHandler);
}
else
{
return (new DefaultRouteProviderContextHandler());
}
}
/*******************************************************************************
** Getter for routeAuthenticator
*******************************************************************************/
@ -267,35 +300,4 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
return (this);
}
/*******************************************************************************
** Getter for contextHandler
*******************************************************************************/
public QCodeReference getContextHandler()
{
return (this.contextHandler);
}
/*******************************************************************************
** Setter for contextHandler
*******************************************************************************/
public void setContextHandler(QCodeReference contextHandler)
{
this.contextHandler = contextHandler;
}
/*******************************************************************************
** Fluent setter for contextHandler
*******************************************************************************/
public ProcessBasedRouter withContextHandler(QCodeReference contextHandler)
{
this.contextHandler = contextHandler;
return (this);
}
}

View File

@ -41,7 +41,6 @@ public class ProcessBasedRouterPayload extends QProcessPayload
private Map<String, List<String>> queryParams;
private Map<String, List<String>> formParams;
private Map<String, String> cookies;
private String bodyString;
private Integer statusCode;
private String redirectURL;
@ -411,35 +410,4 @@ public class ProcessBasedRouterPayload extends QProcessPayload
return (this);
}
/*******************************************************************************
** Getter for bodyString
*******************************************************************************/
public String getBodyString()
{
return (this.bodyString);
}
/*******************************************************************************
** Setter for bodyString
*******************************************************************************/
public void setBodyString(String bodyString)
{
this.bodyString = bodyString;
}
/*******************************************************************************
** Fluent setter for bodyString
*******************************************************************************/
public ProcessBasedRouterPayload withBodyString(String bodyString)
{
this.bodyString = bodyString;
return (this);
}
}

View File

@ -31,7 +31,7 @@ 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.session.QSystemUserSession;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProvider;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
import io.javalin.Javalin;
@ -46,16 +46,17 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
** javalin route provider that hosts a path in the http server via a path on
** the file system
*******************************************************************************/
public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface
public class SimpleFileSystemDirectoryRouter extends QJavalinRouteProvider
{
public static final String LOAD_STATIC_FILES_FROM_JAR_PROPERTY = "qqq.javalin.enableStaticFilesFromJar";
private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class);
private final String hostedPath;
private final String fileSystemPath;
private QCodeReference routeAuthenticator;
private QInstance qInstance;
public static boolean loadStaticFilesFromJar = false;
private final String fileSystemPath;
private final String hostedPath;
private QCodeReference routeAuthenticator;
private QInstance qInstance;
private String spaRootPath;
private String spaRootFile;
@ -67,6 +68,24 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
{
this.hostedPath = hostedPath;
this.fileSystemPath = fileSystemPath;
///////////////////////////////////////////////////////////////////////////////////////////////////////
// read the property to see if we should load static files from the jar file or from the file system //
// Javan only supports loading via one method per path, so its a choice of one or the other... //
///////////////////////////////////////////////////////////////////////////////////////////////////////
try
{
String propertyValue = System.getProperty(SimpleFileSystemDirectoryRouter.LOAD_STATIC_FILES_FROM_JAR_PROPERTY, "");
if(propertyValue.equals("true"))
{
loadStaticFilesFromJar = true;
}
}
catch(Exception e)
{
loadStaticFilesFromJar = false;
LOG.warn("Exception attempting to read system property, defaulting to false. ", e, logPair("system property", SimpleFileSystemDirectoryRouter.LOAD_STATIC_FILES_FROM_JAR_PROPERTY));
}
}
@ -77,6 +96,8 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
public SimpleFileSystemDirectoryRouter(JavalinRouteProviderMetaData routeProvider)
{
this(routeProvider.getHostedPath(), routeProvider.getFileSystemPath());
setSpaRootPath(routeProvider.getSpaRootPath());
setSpaRootFile(routeProvider.getSpaRootFile());
setRouteAuthenticator(routeProvider.getRouteAuthenticator());
}
@ -98,25 +119,40 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
***************************************************************************/
private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig)
{
URL resource = getClass().getClassLoader().getResource(fileSystemPath);
if(resource == null)
{
String message = "Could not find file system path: " + fileSystemPath;
if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null)
{
message += ". For non-absolute paths, do not prefix with a leading slash.";
}
throw new RuntimeException(message);
}
if(!hostedPath.startsWith("/"))
{
LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash...");
LOG.warn("hostedPath should probably start with a leading slash...", logPair("hostedPath", hostedPath));
}
staticFileConfig.directory = resource.getFile();
staticFileConfig.hostedPath = hostedPath;
staticFileConfig.location = Location.EXTERNAL;
/////////////////////////////////////////////////////////////////////////////////////////
// Handle loading static files from the jar OR the filesystem based on system property //
/////////////////////////////////////////////////////////////////////////////////////////
if(SimpleFileSystemDirectoryRouter.loadStaticFilesFromJar)
{
staticFileConfig.directory = fileSystemPath;
staticFileConfig.hostedPath = hostedPath;
staticFileConfig.location = Location.CLASSPATH;
}
else
{
URL resource = getClass().getClassLoader().getResource(fileSystemPath);
if(resource == null)
{
String message = "Could not find file system path: " + fileSystemPath;
if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null)
{
message += ". For non-absolute paths, do not prefix with a leading slash.";
}
throw new RuntimeException(message);
}
staticFileConfig.directory = resource.getFile();
staticFileConfig.hostedPath = hostedPath;
staticFileConfig.location = Location.EXTERNAL;
}
LOG.info("Static File Config", logPair("hostedPath", hostedPath), logPair("directory", staticFileConfig.directory), logPair("location", staticFileConfig.location));
}
@ -168,6 +204,10 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
@Override
public void acceptJavalinConfig(JavalinConfig config)
{
if(this.getSpaRootPath() != null && !this.getSpaRootPath().isEmpty())
{
config.spaRoot.addFile(this.spaRootPath, this.spaRootFile);
}
config.staticFiles.add(this::handleJavalinStaticFileConfig);
}
@ -221,4 +261,74 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
return (this);
}
/*******************************************************************************
* Getter for spaRootPath
* @see #withSpaRootPath(String)
*******************************************************************************/
public String getSpaRootPath()
{
return (this.spaRootPath);
}
/*******************************************************************************
* Setter for spaRootPath
* @see #withSpaRootPath(String)
*******************************************************************************/
public void setSpaRootPath(String spaRootPath)
{
this.spaRootPath = spaRootPath;
}
/*******************************************************************************
* Fluent setter for spaRootPath
* @param spaRootPath TODO document this property
* @return this
*******************************************************************************/
public SimpleFileSystemDirectoryRouter withSpaRootPath(String spaRootPath)
{
this.spaRootPath = spaRootPath;
return (this);
}
/*******************************************************************************
* Getter for spaRootFile
* @see #withSpaRootFile(String)
*******************************************************************************/
public String getSpaRootFile()
{
return (this.spaRootFile);
}
/*******************************************************************************
* Setter for spaRootFile
* @see #withSpaRootFile(String)
*******************************************************************************/
public void setSpaRootFile(String spaRootFile)
{
this.spaRootFile = spaRootFile;
}
/*******************************************************************************
* Fluent setter for spaRootFile
* @param spaRootFile TODO document this property
* @return this
*******************************************************************************/
public SimpleFileSystemDirectoryRouter withSpaRootFile(String spaRootFile)
{
this.spaRootFile = spaRootFile;
return (this);
}
}

View File

@ -1,159 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.middleware.javalin.routeproviders.contexthandlers;
import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.actions.tables.storage.StorageInput;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
/*******************************************************************************
** default implementation of this interface. reads the request body as a string
*******************************************************************************/
public class DefaultRouteProviderContextHandler implements RouteProviderContextHandlerInterface
{
/***************************************************************************
**
***************************************************************************/
@Override
public void handleRequest(Context context, RunProcessInput input)
{
input.addValue("path", context.path());
input.addValue("method", context.method());
input.addValue("pathParams", new HashMap<>(context.pathParamMap()));
input.addValue("queryParams", new HashMap<>(context.queryParamMap()));
input.addValue("cookies", new HashMap<>(context.cookieMap()));
input.addValue("requestHeaders", new HashMap<>(context.headerMap()));
handleRequestBody(context, input);
}
/***************************************************************************
**
***************************************************************************/
protected void handleRequestBody(Context context, RunProcessInput input)
{
input.addValue("formParams", new HashMap<>(context.formParamMap()));
input.addValue("bodyString", context.body());
}
/***************************************************************************
**
***************************************************************************/
@Override
public boolean handleResponse(Context context, RunProcessOutput runProcessOutput) throws QException
{
handleResponseHeaders(context, runProcessOutput);
//////////////
// response //
//////////////
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
String redirectURL = runProcessOutput.getValueString("redirectURL");
if(StringUtils.hasContent(redirectURL))
{
context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode));
return true;
}
if(statusCode != null)
{
context.status(statusCode);
}
if(handleResponseBody(context, runProcessOutput))
{
return true;
}
return false;
}
/***************************************************************************
**
***************************************************************************/
protected void handleResponseHeaders(Context context, RunProcessOutput runProcessOutput)
{
/////////////////
// headers map //
/////////////////
Serializable headers = runProcessOutput.getValue("responseHeaders");
if(headers instanceof Map headersMap)
{
for(Object key : headersMap.keySet())
{
context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key)));
}
}
}
/***************************************************************************
**
***************************************************************************/
protected boolean handleResponseBody(Context context, RunProcessOutput runProcessOutput) throws QException
{
String responseString = runProcessOutput.getValueString("responseString");
byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes");
StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput");
if(StringUtils.hasContent(responseString))
{
context.result(responseString);
return true;
}
if(responseBytes != null && responseBytes.length > 0)
{
context.result(responseBytes);
return true;
}
if(responseStorageInput != null)
{
InputStream inputStream = new StorageAction().getInputStream(responseStorageInput);
context.result(inputStream);
return true;
}
return false;
}
}

View File

@ -1,49 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.middleware.javalin.routeproviders.contexthandlers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import io.javalin.http.Context;
/*******************************************************************************
** interface for how to handle the javalin context for a process based route provider.
** e.g., taking things like query params and the request body into the process input
** and similarly for the http response from the process output..
*******************************************************************************/
public interface RouteProviderContextHandlerInterface
{
/***************************************************************************
**
***************************************************************************/
void handleRequest(Context context, RunProcessInput runProcessInput);
/***************************************************************************
**
***************************************************************************/
boolean handleResponse(Context context, RunProcessOutput runProcessOutput) throws QException;
}

View File

@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.javalin.TestUtils;
import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import io.javalin.http.HttpStatus;
import kong.unirest.HttpResponse;
@ -36,6 +37,7 @@ import kong.unirest.Unirest;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter.LOAD_STATIC_FILES_FROM_JAR_PROPERTY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -52,6 +54,16 @@ class QApplicationJavalinServerTest
/***************************************************************************
**
***************************************************************************/
private static AbstractQQQApplication getQqqApplication()
{
return new TestApplication();
}
/*******************************************************************************
**
*******************************************************************************/
@ -60,6 +72,7 @@ class QApplicationJavalinServerTest
{
javalinServer.stop();
TestApplication.callCount = 0;
System.clearProperty(LOAD_STATIC_FILES_FROM_JAR_PROPERTY);
}
@ -196,6 +209,128 @@ class QApplicationJavalinServerTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStaticRouterFilesFromExternal() throws Exception
{
System.setProperty(LOAD_STATIC_FILES_FROM_JAR_PROPERTY, "false");
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT);
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8");
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/statically-served/foo.html").asString();
assertEquals("Foo? Bar!", response.getBody());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFrontendMaterialDashboardHostedPathDefault() throws Exception
{
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(true)
.withPort(PORT)
.withFrontendMaterialDashboardHostedPath("/");
javalinServer.start();
//////////////////////////////////////////////////////
// Verify that we can get access the file correctly //
//////////////////////////////////////////////////////
Unirest.config().setDefaultResponseEncoding("UTF-8");
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/dashboard.html").asString();
assertEquals(200, response.getStatus());
assertEquals("This is a mock of /material-dashboard/dashboard.html for testing purposes.", response.getBody());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFrontendMaterialDashboardHostedPathDefaultInverse() throws Exception
{
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(true)
.withPort(PORT)
.withFrontendMaterialDashboardHostedPath("/");
javalinServer.start();
////////////////////////////////////////////////////////////
// Verify that the file is not accessible at the app path //
////////////////////////////////////////////////////////////
Unirest.config().setDefaultResponseEncoding("UTF-8");
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/bs-directory/dashboard.html").asString();
/////////////////////////////////////////////////////////////////////////////////
// Note, this will not 404, instead it will return the default index.html file //
/////////////////////////////////////////////////////////////////////////////////
assertEquals("This is a mock of /material-dashboard/index.html for testing purposes.", response.getBody());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFrontendMaterialDashboardHostedPathCustomApp() throws Exception
{
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(true)
.withPort(PORT)
.withFrontendMaterialDashboardHostedPath("/app");
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8");
//////////////////////////////////////////////////////
// verify that we can get access the file correctly //
//////////////////////////////////////////////////////
HttpResponse<String> response1 = Unirest.get("http://localhost:" + PORT + "/app/dashboard.html").asString();
assertEquals(200, response1.getStatus());
assertEquals("This is a mock of /material-dashboard/dashboard.html for testing purposes.", response1.getBody());
/////////////////////////////////////////////////////////////
// Verify that the file is not accessible at the root path //
/////////////////////////////////////////////////////////////
HttpResponse<String> response2 = Unirest.get("http://localhost:" + PORT + "/index.html").asString();
assertEquals(404, response2.getStatus());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStaticRouterFilesFromClasspath() throws Exception
{
System.setProperty(LOAD_STATIC_FILES_FROM_JAR_PROPERTY, "true");
javalinServer = new QApplicationJavalinServer(new QApplicationJavalinServerTest.TestApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT)
.withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter("/statically-served-from-jar", "static-site-from-jar/"));
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8");
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/statically-served-from-jar/foo-in-jar.html").asString();
assertEquals("Foo in a Jar!\n", response.getBody());
}
/*******************************************************************************
**
*******************************************************************************/
@ -296,16 +431,6 @@ class QApplicationJavalinServerTest
/***************************************************************************
**
***************************************************************************/
private static AbstractQQQApplication getQqqApplication()
{
return new TestApplication();
}
/***************************************************************************
**
***************************************************************************/

View File

@ -0,0 +1 @@
This is a mock of /material-dashboard-overlay/overlay.html for testing purposes.

View File

@ -0,0 +1 @@
This is a mock of /material-dashboard/dashboard.html for testing purposes.

View File

@ -0,0 +1 @@
This is a mock of /material-dashboard/index.html for testing purposes.

View File

@ -68,7 +68,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-material-dashboard</artifactId>
<version>0.24.0</version>
<version>0.26.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>