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,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<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>
<artifactId>qqq-language-support-javascript</artifactId>
<parent>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-parent-project</artifactId>
<version>${revision}</version>
</parent>
<properties>
<!-- props specifically to this module -->
<!-- todo - remove these!! -->
<coverage.instructionCoveredRatioMinimum>0.10</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.10</coverage.classCoveredRatioMinimum>
</properties>
<dependencies>
<!-- other qqq modules deps -->
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 3rd party deps specifically for this module -->
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.4</version>
</dependency>
<!-- Common deps for all qqq modules -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- plugins specifically for this module -->
<!-- none at this time -->
</plugins>
</build>
</project>

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