Adding POST_QUERY_RECORD customizer; formalizing customizers a bit more

This commit is contained in:
2022-08-12 11:39:39 -05:00
parent 965bc5bf29
commit 52121cc4f3
12 changed files with 378 additions and 45 deletions

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Utility to load code for running QQQ customizers.
*******************************************************************************/
public class CustomizerLoader
{
private static final Logger LOG = LogManager.getLogger(CustomizerLoader.class);
/*******************************************************************************
**
*******************************************************************************/
public static Function<?, ?> getTableCustomizerFunction(QTableMetaData table, String customizerName)
{
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
if(codeReference.isPresent())
{
return (CustomizerLoader.getFunction(codeReference.get()));
}
return null;
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T, R> Function<T, R> getFunction(QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((Function<T, R>) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
}

View File

@ -0,0 +1,32 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
/*******************************************************************************
** Standard place where the names of QQQ Customization points are defined.
*******************************************************************************/
public interface Customizers
{
String POST_QUERY_RECORD = "postQueryRecord";
}

View File

@ -57,7 +57,7 @@ public class QInstanceValidationException extends QException
{
super(
(reasons != null && reasons.size() > 0)
? "Instance validation failed for the following reasons: " + StringUtils.joinWithCommasAndAnd(reasons)
? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons)
: "Validation failed, but no reasons were provided");
if(reasons != null && reasons.size() > 0)

View File

@ -24,8 +24,13 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.CustomizerLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.Customizers;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -34,8 +39,12 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
*******************************************************************************/
public class QueryOutput extends AbstractActionOutput implements Serializable
{
private static final Logger LOG = LogManager.getLogger(QueryOutput.class);
private QueryOutputStorageInterface storage;
private Function<QRecord, QRecord> postQueryRecordCustomizer;
/*******************************************************************************
@ -52,6 +61,8 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
{
storage = new QueryOutputList();
}
postQueryRecordCustomizer = (Function<QRecord, QRecord>) CustomizerLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD);
}
@ -65,16 +76,36 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
*******************************************************************************/
public void addRecord(QRecord record)
{
record = runPostQueryRecordCustomizer(record);
storage.addRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/
public QRecord runPostQueryRecordCustomizer(QRecord record)
{
if(this.postQueryRecordCustomizer != null)
{
record = this.postQueryRecordCustomizer.apply(record);
}
return record;
}
/*******************************************************************************
** add a list of records to this output
*******************************************************************************/
public void addRecords(List<QRecord> records)
{
if(this.postQueryRecordCustomizer != null)
{
records.replaceAll(t -> this.postQueryRecordCustomizer.apply(t));
}
storage.addRecords(records);
}

View File

@ -408,12 +408,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
}
QCodeReference function = customizers.get(customizerName);
if(function == null)
{
throw (new IllegalArgumentException("Customizer [" + customizerName + "] was not found in table [" + name + "]."));
}
return (Optional.of(function));
return (Optional.ofNullable(function));
}

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.Customizers;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
@ -40,6 +42,8 @@ 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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
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.utils.TestUtils;
@ -90,28 +94,9 @@ class MemoryBackendModuleTest
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(session);
insertInput.setTableName(table.getName());
insertInput.setRecords(List.of(
new QRecord()
.withTableName(table.getName())
.withValue("name", "My Triangle")
.withValue("type", "triangle")
.withValue("noOfSides", 3)
.withValue("isPolygon", true),
new QRecord()
.withTableName(table.getName())
.withValue("name", "Your Square")
.withValue("type", "square")
.withValue("noOfSides", 4)
.withValue("isPolygon", true),
new QRecord()
.withTableName(table.getName())
.withValue("name", "Some Circle")
.withValue("type", "circle")
.withValue("noOfSides", null)
.withValue("isPolygon", false)
));
insertInput.setRecords(getTestRecords(table));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(insertOutput.getRecords().size(), 3);
assertEquals(3, insertOutput.getRecords().size());
assertTrue(insertOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null));
assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2)));
@ -124,7 +109,7 @@ class MemoryBackendModuleTest
queryInput.setSession(session);
queryInput.setTableName(table.getName());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(queryOutput.getRecords().size(), 3);
assertEquals(3, queryOutput.getRecords().size());
assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2)));
@ -152,10 +137,10 @@ class MemoryBackendModuleTest
.withValue("type", "ellipse")
));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertEquals(updateOutput.getRecords().size(), 2);
assertEquals(2, updateOutput.getRecords().size());
queryOutput = new QueryAction().execute(queryInput);
assertEquals(queryOutput.getRecords().size(), 3);
assertEquals(3, queryOutput.getRecords().size());
assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("name").equals("My Triangle")));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Not My Triangle any more")));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("type").equals("ellipse")));
@ -171,15 +156,104 @@ class MemoryBackendModuleTest
deleteInput.setTableName(table.getName());
deleteInput.setPrimaryKeys(List.of(1, 2));
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
assertEquals(deleteOutput.getDeletedRecordCount(), 2);
assertEquals(2, deleteOutput.getDeletedRecordCount());
assertEquals(1, new CountAction().execute(countInput).getCount());
queryOutput = new QueryAction().execute(queryInput);
assertEquals(queryOutput.getRecords().size(), 1);
assertEquals(1, queryOutput.getRecords().size());
assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(1)));
assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(2)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> getTestRecords(QTableMetaData table)
{
return List.of(
new QRecord()
.withTableName(table.getName())
.withValue("name", "My Triangle")
.withValue("type", "triangle")
.withValue("noOfSides", 3)
.withValue("isPolygon", true),
new QRecord()
.withTableName(table.getName())
.withValue("name", "Your Square")
.withValue("type", "square")
.withValue("noOfSides", 4)
.withValue("isPolygon", true),
new QRecord()
.withTableName(table.getName())
.withValue("name", "Some Circle")
.withValue("type", "circle")
.withValue("noOfSides", null)
.withValue("isPolygon", false)
);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testCustomizer() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
QSession session = new QSession();
///////////////////////////////////
// add a customizer to the table //
///////////////////////////////////
table.withCustomizer(Customizers.POST_QUERY_RECORD, new QCodeReference(ShapeTestCustomizer.class, QCodeUsage.CUSTOMIZER));
//////////////////
// do an insert //
//////////////////
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(session);
insertInput.setTableName(table.getName());
insertInput.setRecords(getTestRecords(table));
new InsertAction().execute(insertInput);
///////////////////////////////////////////////////////
// do a query - assert that the customizer did stuff //
///////////////////////////////////////////////////////
ShapeTestCustomizer.executionCount = 0;
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
queryInput.setTableName(table.getName());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertEquals(3, ShapeTestCustomizer.executionCount);
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1) && r.getValueInteger("tenTimesId").equals(10)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2) && r.getValueInteger("tenTimesId").equals(20)));
assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3) && r.getValueInteger("tenTimesId").equals(30)));
}
/*******************************************************************************
**
*******************************************************************************/
public static class ShapeTestCustomizer implements Function<QRecord, QRecord>
{
static int executionCount = 0;
@Override
public QRecord apply(QRecord record)
{
executionCount++;
record.setValue("tenTimesId", record.getValueInteger("id") * 10);
return (record);
}
}
}