Compare commits

..

3 Commits

8 changed files with 209 additions and 708 deletions

View File

@ -48,7 +48,11 @@ commands:
- run:
name: Run Maven Verify
command: |
mvn -s .circleci/mvn-settings.xml -T4 verify
mvn -s .circleci/mvn-settings.xml -T4 verify | tee mvn-verify.log
- store_artifacts:
name: Full Maven Verify Log
path: mvn-verify.log
destination: mvn-verify
- store_jacoco_site:
module: qqq-backend-core
- store_jacoco_site:

View File

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

View File

@ -1,280 +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.model.metadata.fields;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for WhiteSpaceBehavior
*******************************************************************************/
class WhiteSpaceBehaviorTest extends BaseTest
{
public static final String FIELD = "firstName";
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNone()
{
assertNull(applyToRecord(WhiteSpaceBehavior.NONE, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertNull(applyToRecord(WhiteSpaceBehavior.NONE, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals("John", applyToRecord(WhiteSpaceBehavior.NONE, new QRecord().withValue(FIELD, "John"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals(ListBuilder.of("J. ohn", null, "Jane\n"), applyToRecords(WhiteSpaceBehavior.NONE, List.of(
new QRecord().withValue(FIELD, "J. ohn"),
new QRecord(),
new QRecord().withValue(FIELD, "Jane\n")),
ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRemoveWhiteSpace()
{
assertNull(applyToRecord(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertNull(applyToRecord(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals("doobeedoobeedoo", applyToRecord(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, new QRecord().withValue(FIELD, "doo bee doo\n bee doo"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals(ListBuilder.of("thisistheway", null, "thatwastheway"), applyToRecords(WhiteSpaceBehavior.REMOVE_ALL_WHITESPACE, List.of(
new QRecord().withValue(FIELD, "this is\rthe way \t"),
new QRecord(),
new QRecord().withValue(FIELD, "that was the way\n")),
ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTrimWhiteSpace()
{
assertNull(applyToRecord(WhiteSpaceBehavior.TRIM, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertNull(applyToRecord(WhiteSpaceBehavior.TRIM, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals("doo bee doo\n bee doo", applyToRecord(WhiteSpaceBehavior.TRIM, new QRecord().withValue(FIELD, " doo bee doo\n bee doo\r \n\n"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals(ListBuilder.of("this is\rthe way", null, "that was the way"), applyToRecords(WhiteSpaceBehavior.TRIM, List.of(
new QRecord().withValue(FIELD, " this is\rthe way \t"),
new QRecord(),
new QRecord().withValue(FIELD, "that was the way\n")),
ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTrimLeftWhiteSpace()
{
assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_LEFT, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_LEFT, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals("doo bee doo\n bee doo\r \n\n", applyToRecord(WhiteSpaceBehavior.TRIM_LEFT, new QRecord().withValue(FIELD, " doo bee doo\n bee doo\r \n\n"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals(ListBuilder.of("this is\rthe way \t", null, "that was the way\n"), applyToRecords(WhiteSpaceBehavior.TRIM_LEFT, List.of(
new QRecord().withValue(FIELD, " this is\rthe way \t"),
new QRecord(),
new QRecord().withValue(FIELD, " \n that was the way\n")),
ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTrimRightWhiteSpace()
{
assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_RIGHT, new QRecord(), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertNull(applyToRecord(WhiteSpaceBehavior.TRIM_RIGHT, new QRecord().withValue(FIELD, null), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals(" doo bee doo\n bee doo", applyToRecord(WhiteSpaceBehavior.TRIM_RIGHT, new QRecord().withValue(FIELD, " doo bee doo\n bee doo\r \n\n"), ValueBehaviorApplier.Action.INSERT).getValue(FIELD));
assertEquals(ListBuilder.of(" this is\rthe way", null, " \n that was the way"), applyToRecords(WhiteSpaceBehavior.TRIM_RIGHT, List.of(
new QRecord().withValue(FIELD, " this is\rthe way \t"),
new QRecord(),
new QRecord().withValue(FIELD, " \n that was the way\n")),
ValueBehaviorApplier.Action.INSERT).stream().map(r -> r.getValueString(FIELD)).toList());
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord applyToRecord(WhiteSpaceBehavior behavior, QRecord record, ValueBehaviorApplier.Action action)
{
return (applyToRecords(behavior, List.of(record), action).get(0));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> applyToRecords(WhiteSpaceBehavior behavior, List<QRecord> records, ValueBehaviorApplier.Action action)
{
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
behavior.apply(action, records, QContext.getQInstance(), table, table.getField(FIELD));
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testReads() throws QException
{
TestUtils.insertDefaultShapes(QContext.getQInstance());
List<QRecord> records = QueryAction.execute(TestUtils.TABLE_NAME_SHAPE, null);
assertEquals(Set.of("Triangle", "Square", "Circle"), records.stream().map(r -> r.getValueString("name")).collect(Collectors.toSet()));
QFieldMetaData field = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE).getField("name");
field.setBehaviors(Set.of(CaseChangeBehavior.TO_UPPER_CASE));
records = QueryAction.execute(TestUtils.TABLE_NAME_SHAPE, null);
assertEquals(Set.of("TRIANGLE", "SQUARE", "CIRCLE"), records.stream().map(r -> r.getValueString("name")).collect(Collectors.toSet()));
field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE));
assertEquals("triangle", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, 1).getValueString("name"));
field.setBehaviors(Set.of(CaseChangeBehavior.NONE));
assertEquals("Triangle", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, 1).getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testWrites() throws QException
{
Integer id = 100;
QFieldMetaData field = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE).getField("name");
field.setBehaviors(Set.of(CaseChangeBehavior.TO_UPPER_CASE));
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", id).withValue("name", "Octagon")));
//////////////////////////////////////////////////////////////////////////////////
// turn off the to-upper-case behavior, so we'll see what was actually inserted //
//////////////////////////////////////////////////////////////////////////////////
field.setBehaviors(Collections.emptySet());
assertEquals("OCTAGON", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, id).getValueString("name"));
////////////////////////////////////////////
// change to toLowerCase and do an update //
////////////////////////////////////////////
field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE));
new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", id).withValue("name", "Octagon")));
////////////////////////////////////////////////////////////////////////////////////
// turn off the to-lower-case behavior, so we'll see what was actually updated to //
////////////////////////////////////////////////////////////////////////////////////
field.setBehaviors(Collections.emptySet());
assertEquals("octagon", GetAction.execute(TestUtils.TABLE_NAME_SHAPE, id).getValueString("name"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFilter()
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
QFieldMetaData field = table.getField("name");
field.setBehaviors(Set.of(CaseChangeBehavior.TO_UPPER_CASE));
assertEquals("SQUARE", CaseChangeBehavior.TO_UPPER_CASE.applyToFilterCriteriaValue("square", qInstance, table, field));
field.setBehaviors(Set.of(CaseChangeBehavior.TO_LOWER_CASE));
assertEquals("triangle", CaseChangeBehavior.TO_LOWER_CASE.applyToFilterCriteriaValue("Triangle", qInstance, table, field));
field.setBehaviors(Set.of(CaseChangeBehavior.NONE));
assertEquals("Circle", CaseChangeBehavior.NONE.applyToFilterCriteriaValue("Circle", qInstance, table, field));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testValidation()
{
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_SHAPE);
///////////////////////////////////////////
// should be no errors on a string field //
///////////////////////////////////////////
assertTrue(CaseChangeBehavior.TO_UPPER_CASE.validateBehaviorConfiguration(table, table.getField("name")).isEmpty());
//////////////////////////////////////////
// should be an error on a number field //
//////////////////////////////////////////
assertEquals(1, CaseChangeBehavior.TO_LOWER_CASE.validateBehaviorConfiguration(table, table.getField("id")).size());
/////////////////////////////////////////
// NONE should be allowed on any field //
/////////////////////////////////////////
assertTrue(CaseChangeBehavior.NONE.validateBehaviorConfiguration(table, table.getField("id")).isEmpty());
}
}

View File

@ -47,9 +47,14 @@
<!-- 3rd party deps specifically for this module -->
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.4</version>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>22.3.0</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>22.3.0</version>
</dependency>
<!-- Common deps for all qqq modules -->

View File

@ -21,11 +21,9 @@
package com.kingsrook.qqq.languages.javascript;
// Javax imports removed for GraalVM migration
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant;
@ -39,14 +37,11 @@ import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLogg
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.NotImplementedException;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import org.openjdk.nashorn.internal.runtime.ECMAException;
import org.openjdk.nashorn.internal.runtime.ParserException;
import org.openjdk.nashorn.internal.runtime.Undefined;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
/*******************************************************************************
@ -91,57 +86,85 @@ public class QJavaScriptExecutor implements QCodeExecutor
{
return (new BigDecimal(d));
}
else if(object instanceof Undefined)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// well, we always said we wanted javascript to treat null & undefined the same way... here's our chance //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
if(object instanceof ScriptObjectMirror scriptObjectMirror)
else if(object instanceof Value val)
{
try
{
if("Date".equals(scriptObjectMirror.getClassName()))
//////////////////////////////////////////////////
// treat JavaScript null/undefined as Java null //
//////////////////////////////////////////////////
if(val.isNull() || val.isHostObject() && val.asHostObject() == null)
{
////////////////////////////////////////////////////////////////////
// looks like the js Date is in UTC (is that because our JVM is?) //
// so the instant being in UTC matches //
////////////////////////////////////////////////////////////////////
Double millis = (Double) scriptObjectMirror.callMember("getTime");
Instant instant = Instant.ofEpochMilli(millis.longValue());
return (instant);
return null;
}
////////////////
// primitives //
////////////////
if(val.isString())
{
return val.asString();
}
if(val.isBoolean())
{
return val.asBoolean();
}
if(val.isNumber())
{
//////////////////////////////////////////
// preserve integer types when possible //
//////////////////////////////////////////
if(val.fitsInInt())
{
return val.asInt();
}
else if(val.fitsInLong())
{
return val.asLong();
}
else
{
return new BigDecimal(val.asDouble());
}
}
//////////////////////////////////////////////
// detect JS Date by existence of getTime() //
//////////////////////////////////////////////
if(val.hasMember("getTime") && val.canInvokeMember("getTime"))
{
double millis = val.invokeMember("getTime").asDouble();
return Instant.ofEpochMilli((long) millis);
}
////////////
// arrays //
////////////
if(val.hasArrayElements())
{
List<Object> result = new ArrayList<>();
long size = val.getArraySize();
for(long i = 0; i < size; i++)
{
result.add(convertObjectToJava(val.getArrayElement(i)));
}
return result;
}
/////////////
// objects //
/////////////
if(val.hasMembers())
{
Map<String, Object> result = new HashMap<>();
for(String key : val.getMemberKeys())
{
result.put(key, convertObjectToJava(val.getMember(key)));
}
return result;
}
}
catch(Exception e)
{
LOG.debug("Error unwrapping javascript date", e);
}
if(scriptObjectMirror.isArray())
{
List<Object> result = new ArrayList<>();
for(String key : scriptObjectMirror.keySet())
{
result.add(Integer.parseInt(key), convertObjectToJava(scriptObjectMirror.get(key)));
}
return (result);
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// last thing we know to try (though really, there's probably some check we should have around this) //
///////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Object> result = new HashMap<>();
for(String key : scriptObjectMirror.keySet())
{
result.put(key, convertObjectToJava(scriptObjectMirror.get(key)));
}
return (result);
LOG.debug("Error converting GraalVM value", e);
}
}
return QCodeExecutor.super.convertObjectToJava(object);
}
catch(Exception e)
@ -165,9 +188,14 @@ public class QJavaScriptExecutor implements QCodeExecutor
{
if(object instanceof Instant i)
{
long millis = (i.getEpochSecond() * 1000 + i.getLong(ChronoField.MILLI_OF_SECOND));
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
return engine.eval("new Date(" + millis + ")");
long millis = (i.getEpochSecond() * 1000 + i.getLong(ChronoField.MILLI_OF_SECOND));
Context context = Context.newBuilder("js")
.allowAllAccess(true)
.allowExperimentalOptions(true)
.option("js.ecmascript-version", "2022")
.build();
Value jsDate = context.eval("js", "new Date(" + millis + ")");
return jsDate.asHostObject();
}
}
@ -186,32 +214,29 @@ public class QJavaScriptExecutor implements QCodeExecutor
*******************************************************************************/
private Serializable runInline(String code, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
{
new NashornScriptEngineFactory();
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
//////////////////////////////////////////////
// setup the javascript environment/context //
//////////////////////////////////////////////
Bindings bindings = engine.createBindings();
bindings.putAll(inputContext);
if(!bindings.containsKey("logger"))
Context context = Context.newBuilder("js")
.allowAllAccess(true)
.allowExperimentalOptions(true)
.option("js.ecmascript-version", "2022")
.build();
// Populate GraalJS bindings from the inputContext
Value bindingsScope = context.getBindings("js");
for(Map.Entry<String, Serializable> entry : inputContext.entrySet())
{
bindings.put("logger", executionLogger);
bindingsScope.putMember(entry.getKey(), entry.getValue());
}
////////////////////////////////////////////////////////////////////////
// wrap the user's code in an immediately-invoked function expression //
// if the user's code (%s below) returns - then our IIFE is done. //
// if the user's code doesn't return, but instead created a 'script' //
// variable, with a 'main' function on it (e.g., from a compiled //
// type script file), then call main function and return its result. //
////////////////////////////////////////////////////////////////////////
// Ensure logger is available
if(!bindingsScope.hasMember("logger"))
{
bindingsScope.putMember("logger", executionLogger);
}
// wrap the user's code in an immediately-invoked function expression
String codeToRun = """
(function userDefinedFunction()
{
'use strict';
%s
var mainFunction = null;
try
{
@ -221,7 +246,7 @@ public class QJavaScriptExecutor implements QCodeExecutor
}
}
catch(e) { }
if(mainFunction != null)
{
return (mainFunction());
@ -232,66 +257,25 @@ public class QJavaScriptExecutor implements QCodeExecutor
Serializable output;
try
{
output = (Serializable) engine.eval(codeToRun, bindings);
Source source = Source.newBuilder("js", codeToRun, "batchName.js")
.mimeType("application/javascript+module")
.build();
Value result = context.eval(source);
output = (Serializable) result.asHostObject();
}
catch(ScriptException se)
catch(Exception se)
{
QCodeException qCodeException = getQCodeExceptionFromScriptException(se);
throw (qCodeException);
// We no longer have ScriptException, so wrap as QCodeException
throw new QCodeException("Error during JavaScript execution: " + se.getMessage(), se);
}
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
private QCodeException getQCodeExceptionFromScriptException(ScriptException se)
{
boolean isParserException = ExceptionUtils.findClassInRootChain(se, ParserException.class) != null;
boolean isUserThrownException = ExceptionUtils.findClassInRootChain(se, ECMAException.class) != null;
String message = se.getMessage();
String errorContext = null;
if(message != null)
{
message = message.replaceFirst(" in <eval>.*", "");
message = message.replaceFirst("<eval>:\\d+:\\d+", "");
if(message.contains("\n"))
{
String[] parts = message.split("\n", 2);
message = parts[0];
errorContext = parts[1];
}
}
int actualScriptLineNumber = se.getLineNumber() - 2;
String prefix = "Script Exception";
boolean includeColumn = true;
boolean includeContext = false;
if(isParserException)
{
prefix = "Script parser exception";
includeContext = true;
}
else if(isUserThrownException)
{
prefix = "Script threw an exception";
includeColumn = false;
}
QCodeException qCodeException = new QCodeException(prefix + " at line " + actualScriptLineNumber + (includeColumn ? (" column " + se.getColumnNumber()) : "") + ": " + message);
if(includeContext)
{
qCodeException.setContext(errorContext);
}
return (qCodeException);
}
// getQCodeExceptionFromScriptException is now unused (ScriptException/Nashorn removed)

View File

@ -1,106 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.api.actions;
import com.kingsrook.qqq.api.model.APIVersionRange;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
** utility methods for working with fields
**
*******************************************************************************/
public class ApiFieldUtils
{
/*******************************************************************************
**
*******************************************************************************/
public static boolean isIncluded(String apiName, QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && BooleanUtils.isTrue(apiFieldMetaData.getIsExcluded()))
{
return (false);
}
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
public static APIVersionRange getApiVersionRangeForRemovedField(String apiName, QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
{
if(StringUtils.hasContent(apiFieldMetaData.getFinalVersion()))
{
return (APIVersionRange.betweenAndIncluding(apiFieldMetaData.getInitialVersion(), apiFieldMetaData.getFinalVersion()));
}
else
{
throw (new IllegalStateException("RemovedApiFieldMetaData for field [" + field.getName() + "] did not specify a finalVersion."));
}
}
else
{
throw (new IllegalStateException("RemovedApiFieldMetaData for field [" + field.getName() + "] did not specify an initialVersion."));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static APIVersionRange getApiVersionRange(String apiName, QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
{
return (APIVersionRange.afterAndIncluding(apiFieldMetaData.getInitialVersion()));
}
return (APIVersionRange.none());
}
/*******************************************************************************
**
*******************************************************************************/
public static ApiFieldMetaData getApiFieldMetaData(String apiName, QFieldMetaData field)
{
return ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData());
}
}

View File

@ -111,7 +111,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
public static final String QUERY_DESCRIPTION = """
Execute a query on this table, using query criteria as specified in query string parameters.
* Pagination is managed via the `pageNo` & `pageSize` query string parameters. pageNo starts at 1. pageSize defaults to 50.
* By default, the response includes the total count of records that match the query criteria. The count can be omitted by specifying `includeCount=false`
* By default, results are sorted by the table's primary key, descending. This can be changed by specifying the `orderBy` query string parameter, following SQL ORDER BY syntax (e.g., `fieldName1 ASC, fieldName2 DESC`)
@ -139,26 +139,26 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
* The request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body.
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
Upon success, a status code of 201 (`Created`) is returned, and the generated value for the primary key will be returned in the response body object.
""";
public static final String UPDATE_DESCRIPTION = """
Update one record in this table, by specifying its primary key as a path parameter, and by supplying values to be updated in the request body.
* Only the fields provided in the request body will be updated.
* To remove a value from a field, supply the key for the field, with a null value.
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
* Note that if the request body includes the primary key, it will be ignored. Only the primary key value path parameter will be used.
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
""";
public static final String DELETE_DESCRIPTION = """
Delete one record from this table, by specifying its primary key as a path parameter.
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
""";
@ -167,7 +167,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
* The objects in the request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
* For sub-status codes of 201 (`Created`), and the generated value for the primary key will be returned in the response body object.
@ -180,7 +180,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
* Any unrecognized field names in the body will cause a 400 error.
* Any read-only (non-editable) fields provided in the body will be silently ignored.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
* Each input object's primary key will also be included in the corresponding response object.
@ -188,7 +188,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
public static final String BULK_DELETE_DESCRIPTION = """
Delete one or more records from this table, by supplying an array of primary key values in the request body.
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
* The 1st primary key in the request will have its response in the 1st object in the response, and so-forth.
* Each input primary key will also be included in the corresponding response object.
@ -726,7 +726,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
ApiProcessMetaData apiProcessMetaData = pair.getA();
QProcessMetaData processMetaData = pair.getB();
addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tableProcessesTag, apiProcessMetaData, processMetaData, apiVersion);
addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tableProcessesTag, apiProcessMetaData, processMetaData);
usedProcessNames.add(processMetaData.getName());
}
@ -761,7 +761,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withName(tag)
.withDescription(tag));
addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tag, apiProcessMetaData, processMetaData, apiVersion);
addProcessEndpoints(qInstance, apiInstanceMetaData, basePath, openAPI, tag, apiProcessMetaData, processMetaData);
usedProcessNames.add(processMetaData.getName());
}
@ -807,14 +807,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private void addProcessEndpoints(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, String basePath, OpenAPI openAPI, String tag, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, APIVersion apiVersion)
private void addProcessEndpoints(QInstance qInstance, ApiInstanceMetaData apiInstanceMetaData, String basePath, OpenAPI openAPI, String tag, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData)
{
String processApiPath = ApiProcessUtils.getProcessApiPath(qInstance, processMetaData, apiProcessMetaData, apiInstanceMetaData);
///////////////////////////
// do the process itself //
///////////////////////////
Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tag), apiVersion);
Path path = generateProcessSpecPathObject(apiInstanceMetaData, apiProcessMetaData, processMetaData, ListBuilder.of(tag));
openAPI.getPaths().put(basePath + processApiPath, path);
///////////////////////////////////////////////////////////////////////
@ -872,7 +872,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private Path generateProcessSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List<String> tags, APIVersion apiVersion)
private Path generateProcessSpecPathObject(ApiInstanceMetaData apiInstanceMetaData, ApiProcessMetaData apiProcessMetaData, QProcessMetaData processMetaData, List<String> tags)
{
String description = apiProcessMetaData.getDescription();
if(!StringUtils.hasContent(description))
@ -927,10 +927,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
for(QFieldMetaData field : CollectionUtils.nonNullList(queryStringParams.getFields()))
{
if(ApiFieldUtils.isIncluded(apiName, field) && ApiFieldUtils.getApiVersionRange(apiName, field).includes(apiVersion))
{
parameters.add(processFieldToParameter(apiInstanceMetaData, field).withIn("query"));
}
parameters.add(processFieldToParameter(apiInstanceMetaData, field).withIn("query"));
}
}

View File

@ -31,9 +31,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.APIVersionRange;
import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsOutput;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaDataContainer;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
@ -43,6 +45,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
@ -156,7 +160,7 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
fieldList.sort(Comparator.comparing(QFieldMetaData::getLabel));
for(QFieldMetaData field : fieldList)
{
if(ApiFieldUtils.isIncluded(input.getApiName(), field) && ApiFieldUtils.getApiVersionRange(input.getApiName(), field).includes(version))
if(!isExcluded(input.getApiName(), field) && getApiVersionRange(input.getApiName(), field).includes(version))
{
fields.add(field);
}
@ -167,7 +171,7 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
//////////////////////////////////////////////////////////////////////////////////////////////////
for(QFieldMetaData field : CollectionUtils.nonNullList(getRemovedApiFields(input.getApiName(), table)))
{
if(ApiFieldUtils.isIncluded(input.getApiName(), field) && ApiFieldUtils.getApiVersionRangeForRemovedField(input.getApiName(), field).includes(version))
if(!isExcluded(input.getApiName(), field) && getApiVersionRangeForRemovedField(input.getApiName(), field).includes(version))
{
fields.add(field);
}
@ -180,6 +184,73 @@ public class GetTableApiFieldsAction extends AbstractQActionFunction<GetTableApi
/*******************************************************************************
**
*******************************************************************************/
private boolean isExcluded(String apiName, QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && BooleanUtils.isTrue(apiFieldMetaData.getIsExcluded()))
{
return (true);
}
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
private APIVersionRange getApiVersionRangeForRemovedField(String apiName, QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
{
if(StringUtils.hasContent(apiFieldMetaData.getFinalVersion()))
{
return (APIVersionRange.betweenAndIncluding(apiFieldMetaData.getInitialVersion(), apiFieldMetaData.getFinalVersion()));
}
else
{
throw (new IllegalStateException("RemovedApiFieldMetaData for field [" + field.getName() + "] did not specify a finalVersion."));
}
}
else
{
throw (new IllegalStateException("RemovedApiFieldMetaData for field [" + field.getName() + "] did not specify an initialVersion."));
}
}
/*******************************************************************************
**
*******************************************************************************/
private APIVersionRange getApiVersionRange(String apiName, QFieldMetaData field)
{
ApiFieldMetaData apiFieldMetaData = getApiFieldMetaData(apiName, field);
if(apiFieldMetaData != null && apiFieldMetaData.getInitialVersion() != null)
{
return (APIVersionRange.afterAndIncluding(apiFieldMetaData.getInitialVersion()));
}
return (APIVersionRange.none());
}
/*******************************************************************************
**
*******************************************************************************/
private static ApiFieldMetaData getApiFieldMetaData(String apiName, QFieldMetaData field)
{
return ObjectUtils.tryAndRequireNonNullElse(() -> ApiFieldMetaDataContainer.of(field).getApiFieldMetaData(apiName), new ApiFieldMetaData());
}
/*******************************************************************************
**
*******************************************************************************/