Initial version of scripts, javascript

This commit is contained in:
2022-10-31 11:35:48 -05:00
parent 0ada444fd4
commit 165583cd98
57 changed files with 7409 additions and 376 deletions

View File

@ -0,0 +1,168 @@
/*
* 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.languages.javascript;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
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.internal.runtime.ECMAException;
import org.openjdk.nashorn.internal.runtime.ParserException;
/*******************************************************************************
**
*******************************************************************************/
public class QJavaScriptExecutor implements QCodeExecutor
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable execute(QCodeReference codeReference, Map<String, Serializable> context, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
{
String code = getCode(codeReference);
Serializable output = runInline(code, context, executionLogger);
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
private Serializable runInline(String code, Map<String, Serializable> context, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
{
new NashornScriptEngineFactory();
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
Bindings bindings = engine.createBindings();
bindings.putAll(context);
if(!bindings.containsKey("logger"))
{
bindings.put("logger", executionLogger);
}
////////////////////////////////////////////////////////////////////////
// wrap the user's code in an immediately-invoked function expression //
////////////////////////////////////////////////////////////////////////
String codeToRun = """
(function userDefinedFunction()
{
%s
})();
""".formatted(code);
Serializable output;
try
{
output = (Serializable) engine.eval(codeToRun, bindings);
}
catch(ScriptException se)
{
QCodeException qCodeException = getQCodeExceptionFromScriptException(se);
throw (qCodeException);
}
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);
}
/*******************************************************************************
**
*******************************************************************************/
private String getCode(QCodeReference codeReference)
{
if(StringUtils.hasContent(codeReference.getInlineCode()))
{
return (codeReference.getInlineCode());
}
else
{
throw (new NotImplementedException("Only inline code is implemented at this time."));
}
}
}

View File

@ -0,0 +1,322 @@
/*
* 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.languages.javascript;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.actions.scripts.ExecuteCodeAction;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
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.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import org.assertj.core.api.Assertions;
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 ExecuteCodeAction
*******************************************************************************/
class ExecuteCodeActionTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testHelloWorld() throws QException
{
ExecuteCodeInput input = new ExecuteCodeInput(TestUtils.defineInstance())
.withCodeReference(new QCodeReference("helloWorld.js", QCodeType.JAVA_SCRIPT, QCodeUsage.CUSTOMIZER)
.withInlineCode("""
return "Hello, " + input"""))
.withContext("input", "World");
ExecuteCodeOutput output = new ExecuteCodeOutput();
new ExecuteCodeAction().run(input, output);
assertEquals("Hello, World", output.getOutput());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSetInContextObject() throws QException
{
OneTestOutput oneTestOutput = testOne(3, """
var a = 1;
var b = 2;
output.setD(a + b + input.getC());
""");
assertEquals(6, oneTestOutput.testOutput().getD());
assertNull(oneTestOutput.executeCodeOutput().getOutput());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testReturnsContextObject() throws QException
{
OneTestOutput oneTestOutput = testOne(4, """
var a = 1;
var b = 2;
output.setD(a + b + input.getC());
return (output);
""");
assertEquals(7, oneTestOutput.testOutput().getD());
assertTrue(oneTestOutput.executeCodeOutput().getOutput() instanceof TestOutput);
assertEquals(7, ((TestOutput) oneTestOutput.executeCodeOutput().getOutput()).getD());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testReturnsPrimitive() throws QException
{
OneTestOutput oneTestOutput = testOne(5, """
var a = 1;
var b = 2;
output.setD(a + b + input.getC());
return output.getD()
""");
assertEquals(8, oneTestOutput.testOutput().getD());
assertEquals(8, oneTestOutput.executeCodeOutput().getOutput());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testThrows() throws QException
{
String code = """
var a = 1;
var b = 2;
if (input.getC() === 6)
{
throw ("oh no, six!");
}
output.setD(a + b + input.getC());
return output.getD()
""";
Assertions.assertThatThrownBy(() -> testOne(6, code))
.isInstanceOf(QCodeException.class)
.hasMessageContaining("threw")
.hasMessageContaining("oh no, six!")
.hasMessageContaining("line 5:");
OneTestOutput oneTestOutput = testOne(7, code);
assertEquals(10, oneTestOutput.testOutput().getD());
assertEquals(10, oneTestOutput.executeCodeOutput().getOutput());
Assertions.assertThatThrownBy(() -> testOne(6, """
var a = null;
return a.toString();
"""))
.isInstanceOf(QCodeException.class)
.hasMessageContaining("threw")
.hasMessageContaining("TypeError: null has no such function \"toString\"");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSyntaxError() throws QException
{
Assertions.assertThatThrownBy(() -> testOne(6, """
var a = 1;
if (input.getC() === 6
{
"""))
.isInstanceOf(QCodeException.class)
.hasMessageContaining("parser")
.hasMessageContaining("line 3 column 0");
Assertions.assertThatThrownBy(() -> testOne(6, """
var a = 1;
vr b = 2;
"""))
.isInstanceOf(QCodeException.class)
.hasMessageContaining("parser")
.hasMessageContaining("line 2 column 3");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testLogs() throws QException
{
OneTestOutput oneTestOutput = testOne(5, """
logger.log("This is a log.");
""");
}
/*******************************************************************************
**
*******************************************************************************/
private OneTestOutput testOne(Integer inputValueC, String code) throws QException
{
System.out.println();
QInstance instance = TestUtils.defineInstance();
TestInput testInput = new TestInput();
testInput.setC(inputValueC);
TestOutput testOutput = new TestOutput();
ExecuteCodeInput input = new ExecuteCodeInput(instance);
input.setSession(new QSession());
input.setCodeReference(new QCodeReference("test.js", QCodeType.JAVA_SCRIPT, QCodeUsage.CUSTOMIZER).withInlineCode(code));
input.withContext("input", testInput);
input.withContext("output", testOutput);
ExecuteCodeOutput output = new ExecuteCodeOutput();
ExecuteCodeAction executeCodeAction = new ExecuteCodeAction();
executeCodeAction.run(input, output);
return (new OneTestOutput(output, testOutput));
}
/*******************************************************************************
**
*******************************************************************************/
private record OneTestOutput(ExecuteCodeOutput executeCodeOutput, TestOutput testOutput)
{
}
/*******************************************************************************
**
*******************************************************************************/
public static class TestInput implements Serializable
{
private Integer c;
/*******************************************************************************
** Getter for c
**
*******************************************************************************/
public Integer getC()
{
return c;
}
/*******************************************************************************
** Setter for c
**
*******************************************************************************/
public void setC(Integer c)
{
this.c = c;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "TestInput{c=" + c + '}';
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class TestOutput implements Serializable
{
private Integer d;
/*******************************************************************************
** Getter for d
**
*******************************************************************************/
public Integer getD()
{
return d;
}
/*******************************************************************************
** Setter for d
**
*******************************************************************************/
public void setD(Integer d)
{
this.d = d;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "TestOutput{d=" + d + '}';
}
}
}

View File

@ -0,0 +1,100 @@
/*
* 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.languages.javascript;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class TestUtils
{
public static final String DEFAULT_BACKEND_NAME = "memoryBackend";
/*******************************************************************************
**
*******************************************************************************/
public static QInstance defineInstance()
{
QInstance qInstance = new QInstance();
qInstance.addBackend(defineBackend());
qInstance.addTable(defineTablePerson());
qInstance.setAuthentication(defineAuthentication());
return (qInstance);
}
/*******************************************************************************
** Define the authentication used in standard tests - using 'mock' type.
**
*******************************************************************************/
public static QAuthenticationMetaData defineAuthentication()
{
return new QAuthenticationMetaData()
.withName("mock")
.withType(QAuthenticationType.MOCK);
}
/*******************************************************************************
**
*******************************************************************************/
public static QBackendMetaData defineBackend()
{
return (new QBackendMetaData()
.withName(DEFAULT_BACKEND_NAME)
.withBackendType("memory"));
}
/*******************************************************************************
**
*******************************************************************************/
private static QTableMetaData defineTablePerson()
{
return new QTableMetaData()
.withName("person")
.withLabel("Person")
.withBackendName(DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("firstName", QFieldType.STRING))
.withField(new QFieldMetaData("lastName", QFieldType.STRING))
.withField(new QFieldMetaData("birthDate", QFieldType.DATE))
.withField(new QFieldMetaData("email", QFieldType.STRING));
}
}