diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java
new file mode 100644
index 00000000..4ec61c9a
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptAction.java
@@ -0,0 +1,198 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Objects;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+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.actions.scripts.RunAdHocRecordScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+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.scripts.Script;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RunAdHocRecordScriptAction
+{
+ // todo! private Map scriptRevisionCache = new HashMap<>();
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void run(RunAdHocRecordScriptInput input, RunAdHocRecordScriptOutput output) throws QException
+ {
+ ActionHelper.validateSession(input);
+
+ ScriptRevision scriptRevision = getScriptRevision(input);
+
+ GetInput getInput = new GetInput();
+ getInput.setTableName(input.getTableName());
+ getInput.setPrimaryKey(input.getRecordPrimaryKey());
+ GetOutput getOutput = new GetAction().execute(getInput);
+ QRecord record = getOutput.getRecord();
+ // todo err if not found
+
+ ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
+ executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
+ executeCodeInput.getInput().put("record", record);
+ executeCodeInput.setContext(new HashMap<>());
+ if(input.getOutputObject() != null)
+ {
+ executeCodeInput.getContext().put("output", input.getOutputObject());
+ }
+
+ if(input.getScriptUtils() != null)
+ {
+ executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils());
+ }
+ else
+ {
+ executeCodeInput.getContext().put("scriptUtils", new ScriptApiUtils());
+ }
+
+ executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
+ executeCodeInput.setExecutionLogger(executionLogger);
+ if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
+ scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
+ scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
+ }
+
+ ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
+ new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
+
+ output.setOutput(executeCodeOutput.getOutput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private ScriptRevision getScriptRevision(RunAdHocRecordScriptInput input) throws QException
+ {
+ // todo if(!scriptRevisionCache.containsKey(input.getCodeReference()))
+ {
+ Serializable scriptId = input.getCodeReference().getScriptId();
+ /*
+ if(scriptId == null)
+ {
+ throw (new QNotFoundException("The input record [" + input.getCodeReference().getScriptId() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ + "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
+ }
+
+ */
+
+ Script script = getScript(input, scriptId);
+ /* todo
+ if(script.getCurrentScriptRevisionId() == null)
+ {
+ throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ + "] (scriptId=" + scriptId + ") does not have a current version."));
+ }
+
+ */
+
+ ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
+ // scriptRevisionCache.put(input.getCodeReference(), scriptRevision);
+ return scriptRevision;
+ }
+
+ // return scriptRevisionCache.get(input.getCodeReference());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private ScriptRevision getCurrentScriptRevision(RunAdHocRecordScriptInput input, Serializable scriptRevisionId) throws QException
+ {
+ GetInput getInput = new GetInput();
+ getInput.setTableName("scriptRevision");
+ getInput.setPrimaryKey(scriptRevisionId);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ if(getOutput.getRecord() == null)
+ {
+ /* todo
+ throw (new QNotFoundException("The current revision of the script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ + input.getCodeReference().getFieldName() + "] (scriptRevisionId=" + scriptRevisionId + ") was not found."));
+
+ */
+ throw (new IllegalStateException("todo"));
+ }
+
+ return (new ScriptRevision(getOutput.getRecord()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Script getScript(RunAdHocRecordScriptInput input, Serializable scriptId) throws QException
+ {
+ GetInput getInput = new GetInput();
+ getInput.setTableName("script");
+ getInput.setPrimaryKey(scriptId);
+ GetOutput getOutput = new GetAction().execute(getInput);
+
+ if(getOutput.getRecord() == null)
+ {
+ /*
+ throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ + input.getCodeReference().getFieldName() + "] (script id=" + scriptId + ") was not found."));
+
+ */
+ throw (new IllegalStateException("todo"));
+ }
+
+ return (new Script(getOutput.getRecord()));
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ScriptApiUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ScriptApiUtils.java
new file mode 100644
index 00000000..d4b5631e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ScriptApiUtils.java
@@ -0,0 +1,111 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+
+
+/*******************************************************************************
+
+ $utils.query("order", null);
+ $utils.query($utils.newQueryInput().withTable("order").withLimit(1).withShouldGenerateDisplayValues())
+ *******************************************************************************/
+public class ScriptApiUtils implements Serializable
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QueryInput newQueryInput()
+ {
+ return (new QueryInput());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QRecord newQRecord()
+ {
+ return (new QRecord());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public List query(String table, QQueryFilter filter) throws QException
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(table);
+ queryInput.setFilter(filter);
+ return (new QueryAction().execute(queryInput).getRecords());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public List query(QueryInput queryInput) throws QException
+ {
+ return (new QueryAction().execute(queryInput).getRecords());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void update(String table, List recordList) throws QException
+ {
+ UpdateInput updateInput = new UpdateInput();
+ updateInput.setTableName(table);
+ updateInput.setRecords(recordList);
+ new UpdateAction().execute(updateInput);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void update(String table, QRecord record) throws QException
+ {
+ UpdateInput updateInput = new UpdateInput();
+ updateInput.setTableName(table);
+ updateInput.setRecords(List.of(record));
+ new UpdateAction().execute(updateInput);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptInput.java
new file mode 100644
index 00000000..971ff4c0
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptInput.java
@@ -0,0 +1,270 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RunAdHocRecordScriptInput extends AbstractTableActionInput
+{
+ private AdHocScriptCodeReference codeReference;
+ private Map inputValues;
+ private Serializable recordPrimaryKey;
+ private String tableName;
+ private QCodeExecutionLoggerInterface logger;
+
+ private Serializable outputObject;
+
+ private Serializable scriptUtils;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public RunAdHocRecordScriptInput()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for inputValues
+ **
+ *******************************************************************************/
+ public Map getInputValues()
+ {
+ return inputValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inputValues
+ **
+ *******************************************************************************/
+ public void setInputValues(Map inputValues)
+ {
+ this.inputValues = inputValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inputValues
+ **
+ *******************************************************************************/
+ public RunAdHocRecordScriptInput withInputValues(Map inputValues)
+ {
+ this.inputValues = inputValues;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for outputObject
+ **
+ *******************************************************************************/
+ public Serializable getOutputObject()
+ {
+ return outputObject;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for outputObject
+ **
+ *******************************************************************************/
+ public void setOutputObject(Serializable outputObject)
+ {
+ this.outputObject = outputObject;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for outputObject
+ **
+ *******************************************************************************/
+ public RunAdHocRecordScriptInput withOutputObject(Serializable outputObject)
+ {
+ this.outputObject = outputObject;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for logger
+ *******************************************************************************/
+ public QCodeExecutionLoggerInterface getLogger()
+ {
+ return (this.logger);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for logger
+ *******************************************************************************/
+ public void setLogger(QCodeExecutionLoggerInterface logger)
+ {
+ this.logger = logger;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for logger
+ *******************************************************************************/
+ public RunAdHocRecordScriptInput withLogger(QCodeExecutionLoggerInterface logger)
+ {
+ this.logger = logger;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptUtils
+ **
+ *******************************************************************************/
+ public Serializable getScriptUtils()
+ {
+ return scriptUtils;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptUtils
+ **
+ *******************************************************************************/
+ public void setScriptUtils(Serializable scriptUtils)
+ {
+ this.scriptUtils = scriptUtils;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for codeReference
+ *******************************************************************************/
+ public AdHocScriptCodeReference getCodeReference()
+ {
+ return (this.codeReference);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for codeReference
+ *******************************************************************************/
+ public void setCodeReference(AdHocScriptCodeReference codeReference)
+ {
+ this.codeReference = codeReference;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for codeReference
+ *******************************************************************************/
+ public RunAdHocRecordScriptInput withCodeReference(AdHocScriptCodeReference codeReference)
+ {
+ this.codeReference = codeReference;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for recordPrimaryKey
+ *******************************************************************************/
+ public Serializable getRecordPrimaryKey()
+ {
+ return (this.recordPrimaryKey);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for recordPrimaryKey
+ *******************************************************************************/
+ public void setRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for recordPrimaryKey
+ *******************************************************************************/
+ public RunAdHocRecordScriptInput withRecordPrimaryKey(Serializable recordPrimaryKey)
+ {
+ this.recordPrimaryKey = recordPrimaryKey;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ *******************************************************************************/
+ public String getTableName()
+ {
+ return (this.tableName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tableName
+ *******************************************************************************/
+ public void setTableName(String tableName)
+ {
+ this.tableName = tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ *******************************************************************************/
+ public RunAdHocRecordScriptInput withTableName(String tableName)
+ {
+ this.tableName = tableName;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptOutput.java
new file mode 100644
index 00000000..d070916e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/scripts/RunAdHocRecordScriptOutput.java
@@ -0,0 +1,70 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.scripts;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RunAdHocRecordScriptOutput extends AbstractActionOutput
+{
+ private Serializable output;
+
+
+
+ /*******************************************************************************
+ ** Getter for output
+ **
+ *******************************************************************************/
+ public Serializable getOutput()
+ {
+ return output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for output
+ **
+ *******************************************************************************/
+ public void setOutput(Serializable output)
+ {
+ this.output = output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for output
+ **
+ *******************************************************************************/
+ public RunAdHocRecordScriptOutput withOutput(Serializable output)
+ {
+ this.output = output;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AdHocScriptCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AdHocScriptCodeReference.java
new file mode 100644
index 00000000..bab3cc69
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/AdHocScriptCodeReference.java
@@ -0,0 +1,63 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.code;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AdHocScriptCodeReference extends QCodeReference
+{
+ private Integer scriptId;
+
+
+
+ /*******************************************************************************
+ ** Getter for scriptId
+ *******************************************************************************/
+ public Integer getScriptId()
+ {
+ return (this.scriptId);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scriptId
+ *******************************************************************************/
+ public void setScriptId(Integer scriptId)
+ {
+ this.scriptId = scriptId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scriptId
+ *******************************************************************************/
+ public AdHocScriptCodeReference withScriptId(Integer scriptId)
+ {
+ this.scriptId = scriptId;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java
new file mode 100644
index 00000000..bb8f6a26
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/TablesPossibleValueSourceMetaDataProvider.java
@@ -0,0 +1,63 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.tables;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TablesPossibleValueSourceMetaDataProvider
+{
+ public static final String NAME = "tables";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QPossibleValueSource defineTablesPossibleValueSource(QInstance qInstance)
+ {
+ QPossibleValueSource possibleValueSource = new QPossibleValueSource()
+ .withName(NAME)
+ .withType(QPossibleValueSourceType.ENUM)
+ .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
+
+ List> enumValues = new ArrayList<>();
+ for(QTableMetaData table : qInstance.getTables().values())
+ {
+ enumValues.add(new QPossibleValue<>(table.getName(), table.getLabel()));
+ }
+
+ possibleValueSource.withEnumValues(enumValues);
+ return (possibleValueSource);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java
new file mode 100644
index 00000000..f7589aae
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java
@@ -0,0 +1,178 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.scripts;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+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.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** Action to store a new version of a script, associated with a record.
+ **
+ ** If there's never been a script assigned to the record (for the specified field),
+ ** then a new Script record is first inserted.
+ **
+ ** The script referenced by the record is always updated to point at the new
+ ** scriptRevision record that is inserted.
+ **
+ *******************************************************************************/
+public class StoreScriptRevisionProcessStep implements BackendStep
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
+ {
+ ActionHelper.validateSession(input);
+
+ /*
+ QTableMetaData table = input.getTable();
+ Optional optAssociatedScript = table.getAssociatedScripts().stream().filter(as -> as.getFieldName().equals(input.getFieldName())).findFirst();
+ if(optAssociatedScript.isEmpty())
+ {
+ throw (new QException("Field to update associated script for is not an associated script field."));
+ }
+ AssociatedScript associatedScript = optAssociatedScript.get();
+
+ /////////////////////////////////////////////////////////////
+ // get the record that the script is to be associated with //
+ /////////////////////////////////////////////////////////////
+ QRecord associatedRecord;
+ {
+ GetInput getInput = new GetInput();
+ getInput.setTableName(input.getTableName());
+ getInput.setPrimaryKey(input.getRecordPrimaryKey());
+ getInput.setShouldGenerateDisplayValues(true);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ associatedRecord = getOutput.getRecord();
+ }
+ if(associatedRecord == null)
+ {
+ throw (new QException("Record to associated with script was not found."));
+ }
+ */
+
+ //////////////////////////////////////////////////////////////////
+ // check if there's currently a script referenced by the record //
+ //////////////////////////////////////////////////////////////////
+ Integer scriptId = input.getValueInteger("scriptId");
+ Integer nextSequenceNo = 1;
+
+ ////////////////////////////////////////
+ // get the existing script, to update //
+ ////////////////////////////////////////
+ GetInput getInput = new GetInput();
+ getInput.setTableName("script");
+ getInput.setPrimaryKey(scriptId);
+ GetOutput getOutput = new GetAction().execute(getInput);
+ QRecord script = getOutput.getRecord();
+
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName("scriptRevision");
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
+ .withOrderBy(new QFilterOrderBy("sequenceNo", false))
+ );
+ queryInput.setLimit(1);
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ if(!queryOutput.getRecords().isEmpty())
+ {
+ nextSequenceNo = queryOutput.getRecords().get(0).getValueInteger("sequenceNo") + 1;
+ }
+
+ //////////////////////////////////
+ // insert a new script revision //
+ //////////////////////////////////
+ String commitMessage = input.getValueString("commitMessage");
+ if(!StringUtils.hasContent(commitMessage))
+ {
+ if(nextSequenceNo == 1)
+ {
+ commitMessage = "Initial version";
+ }
+ else
+ {
+ commitMessage = "No commit message given";
+ }
+ }
+
+ QRecord scriptRevision = new QRecord()
+ .withValue("scriptId", script.getValue("id"))
+ .withValue("contents", input.getValueString("contents"))
+ .withValue("commitMessage", commitMessage)
+ .withValue("sequenceNo", nextSequenceNo);
+
+ try
+ {
+ scriptRevision.setValue("author", input.getSession().getUser().getFullName());
+ }
+ catch(Exception e)
+ {
+ scriptRevision.setValue("author", "Unknown");
+ }
+
+ InsertInput insertInput = new InsertInput();
+ insertInput.setTableName("scriptRevision");
+ insertInput.setRecords(List.of(scriptRevision));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+ scriptRevision = insertOutput.getRecords().get(0);
+
+ ////////////////////////////////////////////////////
+ // update the script to point at the new revision //
+ ////////////////////////////////////////////////////
+ script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
+ UpdateInput updateInput = new UpdateInput();
+ updateInput.setTableName("script");
+ updateInput.setRecords(List.of(script));
+ new UpdateAction().execute(updateInput);
+
+ output.addValue("scriptId", script.getValueInteger("id"));
+ output.addValue("scriptName", script.getValueString("name"));
+ output.addValue("scriptRevisionId", scriptRevision.getValueInteger("id"));
+ output.addValue("scriptRevisionSequenceNo", scriptRevision.getValueInteger("sequenceNo"));
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptActionTest.java
new file mode 100644
index 00000000..b8edad0f
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/scripts/RunAdHocRecordScriptActionTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.scripts;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptInput;
+import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.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.AdHocScriptCodeReference;
+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.AssociatedScript;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+
+
+/*******************************************************************************
+ ** Unit test for RunAdHocRecordScriptAction
+ *******************************************************************************/
+class RunAdHocRecordScriptActionTest extends BaseTest
+{
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ setupInstance();
+
+ Integer scriptId = insertScript("""
+ return "Hello";
+ """);
+
+ RunAdHocRecordScriptInput runAdHocRecordScriptInput = new RunAdHocRecordScriptInput();
+ runAdHocRecordScriptInput.setRecordPrimaryKey(1);
+ runAdHocRecordScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAdHocRecordScriptInput.setCodeReference(new AdHocScriptCodeReference().withScriptId(scriptId));
+ runAdHocRecordScriptInput.setLogger(new Log4jCodeExecutionLogger());
+
+ RunAdHocRecordScriptOutput runAdHocRecordScriptOutput = new RunAdHocRecordScriptOutput();
+ new RunAdHocRecordScriptAction().run(runAdHocRecordScriptInput, runAdHocRecordScriptOutput);
+
+ /*
+ RunAssociatedScriptInput runAssociatedScriptInput = new RunAssociatedScriptInput();
+ runAssociatedScriptInput.setInputValues(Map.of());
+ runAssociatedScriptInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ runAssociatedScriptInput.setCodeReference(new AssociatedScriptCodeReference()
+ .withRecordTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withRecordPrimaryKey(1)
+ .withFieldName("testScriptId")
+ );
+ RunAssociatedScriptOutput runAssociatedScriptOutput = new RunAssociatedScriptOutput();
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // ok - since the core module doesn't have the javascript language support module as a dep, this action will fail - but at least we can confirm it fails with this specific exception! //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ assertThatThrownBy(() -> new RunAssociatedScriptAction().run(runAssociatedScriptInput, runAssociatedScriptOutput))
+ .isInstanceOf(QException.class)
+ .hasRootCauseInstanceOf(ClassNotFoundException.class)
+ .hasRootCauseMessage("com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor");
+
+ /////////////////////////////////////
+ // assert that a log was generated //
+ /////////////////////////////////////
+ assertEquals(1, TestUtils.queryTable(ScriptLog.TABLE_NAME).size());
+ */
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void setupInstance() throws QException
+ {
+ QInstance instance = QContext.getQInstance();
+ QTableMetaData personMemory = instance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
+ .withField(new QFieldMetaData("testScriptId", QFieldType.INTEGER))
+ .withAssociatedScript(new AssociatedScript()
+ .withScriptTypeId(1)
+ .withFieldName("testScriptId")
+ );
+
+ new ScriptsMetaDataProvider().defineAll(instance, TestUtils.MEMORY_BACKEND_NAME, null);
+
+ TestUtils.insertRecords(instance, personMemory, List.of(
+ new QRecord().withValue("id", 1),
+ new QRecord().withValue("id", 2)
+ ));
+
+ TestUtils.insertRecords(instance, instance.getTable("scriptType"), List.of(
+ new QRecord().withValue("id", 1).withValue("name", "Test Script Type")
+ ));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Integer insertScript(String code) throws QException
+ {
+ InsertInput insertInput = new InsertInput();
+ insertInput.setTableName("script");
+ insertInput.setRecords(List.of(new QRecord().withValue("name", "Test script")));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+ Integer scriptId = insertOutput.getRecords().get(0).getValueInteger("id");
+
+ insertInput = new InsertInput();
+ insertInput.setTableName("scriptRevision");
+ insertInput.setRecords(List.of(new QRecord().withValue("scriptId", scriptId).withValue("code", code)));
+ insertOutput = new InsertAction().execute(insertInput);
+ Integer scriptRevisionId = insertOutput.getRecords().get(0).getValueInteger("id");
+
+ UpdateInput updateInput = new UpdateInput();
+ updateInput.setTableName("script");
+ updateInput.setRecords(List.of(new QRecord().withValue("id", scriptId).withValue("currentScriptRevisionId", scriptRevisionId)));
+ UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
+
+ return (scriptId);
+ }
+}
\ No newline at end of file