/**
* ContextActions - Actions that manipulate the context
*
* This code is licensed under the MIT License (MIT).
*
* Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI,
* Center for Advanced Visualization and Interaction, Aarhus University
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the “Software”), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
/**
* Actions that change the current context
* @namespace ContextActions
*/
// gate(filter) // filter context
// gate(filter... or=[])
/**
* An action "select" that selects a number of concepts of the given type, optionally filtered by a where condition
* <br />
* Options:
* <ul>
* <li>concept: The concept to select</li>
* <li>property: Select the concepts this property holds</li>
* <li>target: The specific uuid to select</li>
* <li>as: The variable name to save the selection as</li>
* <li>where: A filter spec for filtering on the selection</li>
* <li>forEach (false): If true the select action is run 1 time for each currently selected concept</li>
* <li>stopIfEmpty (false): If true, the action chain stops if nothing is selected</li>
* </ul>
* @memberOf ContextActions
* @example
* //Select all concepts of a type
* {
* "select": {
* "concept": "myConceptType"
* }
* }
*
* @example
* //Select all concepts of a type, and save the selected uuids in the variable $mySelection
* {
* "select": {
* "concept": "myConceptType",
* "as": "mySelection"
* }
* }
*
* @example
* //Select all concepts of a type (Shorthand version)
* {
* "select": "myConceptType"
* }
*
* @example
* //Select concept with given uuid
* {
* "select": {
* "target": "someuuid" //Can also be an array: "target": ["uuid1", "uuid2"]
* }
* }
*
* @example
* //Select concept saved in variable, shorthand
* {
* "select": "$myConceptVariable"
* }
*
* @example
* //Select all concepts of a type, filtering for some property. (Also supports "or", "and" and "not" inside the where clause)
* {
* "select": {
* "concept": "myConceptType",
* "where": {
* "property": "color",
* "equals": "yellow"
* }
* }
* }
*
* @example
* //Select all concepts of a type, filtering using a calculation
* {
* "select": {
* "concept": "myConceptType",
* "where": {
* "calculate": "$myConceptType.myProperty$ + 10",
* "equals": "15"
* }
* }
* }
*
* @example
* //Select all concepts of a type, filtering using lastTarget (only available in forEach)
* {
* "select": {
* "concept": "myConceptType",
* "where": {
* "property": "myConceptType.uuid",
* "equals": "lastTarget.uuid$"
* },
* "forEach": true
* }
* }
*/
class SelectAction extends Action {
static options() {
return {
"$selectType": "enumValue[concept,property,variable]",
"where": "filter",
"as": "@string",
"forEach": "boolean%false",
"stopIfEmpty": "boolean%false"
};
}
constructor(name, options, concept) {
//Handle shorthand
if(typeof options === "string") {
if(options.trim().startsWith("$")) {
options = {
target: options
}
} else {
options = {
concept: options
}
}
}
const defaultOptions = {
forEach: false,
stopIfEmpty: false
};
options = Object.assign({}, defaultOptions, options);
let wherePart = options.where;
delete options.where;
super(name, options, concept);
this.wherePart = wherePart;
}
async apply(contexts, actionArguments) {
const self = this;
const DEBUG_SELECT = false;
async function doSelect(context, options, originalOptions) {
let mark = VarvPerformance.start();
if(DEBUG_SELECT) {
console.group("doSelect");
console.log("Context:", context);
console.log("Options:", options);
console.log("Where:", self.wherePart);
}
let conceptUUIDs = [];
let doFilter = true;
if(options.concept != null) {
//Select concept from type
if(DEBUG_SELECT) {
console.log("Concept selection...");
}
doFilter = false;
let filter = null;
if(self.wherePart) {
let clonedVariables = Object.assign({}, context.variables);
let filterContext = {target: null, lastTarget: context.target, variables: clonedVariables};
let lookupWhereWithArguments = await Action.lookupArguments(self.wherePart, actionArguments);
let lookupWhereOptions = await Action.lookupVariables(lookupWhereWithArguments, filterContext);
filter = await FilterAction.constructFilter(lookupWhereOptions);
}
let limit = 0;
if(options.limit != null) {
limit = options.limit;
}
conceptUUIDs = await VarvEngine.lookupInstances(VarvEngine.getAllImplementingConceptNames(options.concept), filter, context, limit, self.concept);
} else if(options.target != null) {
if(DEBUG_SELECT) console.log("Selecting target:", options.target);
//Select concept from target
if (Array.isArray(options.target)) {
conceptUUIDs.push(...options.target);
} else {
conceptUUIDs.push(options.target);
}
//Filter
let filterResults = conceptUUIDs.map((uuid)=>{
return VarvEngine.getConceptFromUUID(uuid);
});
filterResults = await Promise.all(filterResults);
conceptUUIDs = conceptUUIDs.filter((uuid, index)=>{
let shouldKeep = filterResults[index] != null;
if(!shouldKeep) {
console.warn("Could not find any concept with uuid: ["+uuid+"], ignoring!");
}
return shouldKeep;
});
} else if(options.property != null) {
//Select concept from property of type concept or concept[]
let lookup = await VarvEngine.lookupProperty(context.target, self.concept, options.property);
if(lookup == null) {
if(DEBUG_SELECT) {
console.groupEnd();
}
throw new Error("No property ["+options.property+"] found!");
}
if(lookup.property.isConceptType()) {
let value = await lookup.property.getValue(lookup.target)
conceptUUIDs.push(value);
} else if(lookup.property.isConceptArrayType()) {
let value = await lookup.property.getValue(lookup.target)
conceptUUIDs.push(...value);
} else {
if(DEBUG_SELECT) {
console.groupEnd();
}
throw new Error("Only able to select properties that are of concept or concept array type: ["+options.property+":"+lookup.property.type+"]");
}
} else {
if(DEBUG_SELECT) {
console.groupEnd();
}
throw new Error("Missing option 'concept' or 'target' on select action");
}
//Filtering already done?
if(doFilter) {
let filterMark= VarvPerformance.start();
let filteredUUIDs = [];
if (self.wherePart != null) {
let lookupWhereWithArguments = await Action.lookupArguments(self.wherePart, actionArguments);
let allPromises = [];
for (let uuid of conceptUUIDs) {
let clonedVariables = Object.assign({}, context.variables);
let filterContext = {target: uuid, lastTarget: context.target, variables: clonedVariables};
let lookupWhereOptions = await Action.lookupVariables(lookupWhereWithArguments, filterContext);
let filter = await FilterAction.constructFilter(lookupWhereOptions);
allPromises.push(filter.filter(filterContext, self.concept));
}
let filterResult = await Promise.all(allPromises);
let index = 0;
for (let uuid of conceptUUIDs) {
if(filterResult[index]) {
filteredUUIDs.push(uuid);
}
index++;
}
} else {
filteredUUIDs.push(...conceptUUIDs);
}
VarvPerformance.stop("SelectAction.doSelect.filtering", filterMark, {filter: self.wherePart, numConcepts: conceptUUIDs.length});
conceptUUIDs = filteredUUIDs;
}
let result = null;
if (options.keepContext){
Action.setVariable(context, options.as?options.as:"select", conceptUUIDs);
result = [context];
} else {
result = conceptUUIDs.map((uuid)=>{
// Turn into context
let newContext = Object.assign({}, context, {target: uuid});
//Make sure the target variable from commonVariables, does not bleed out, since we just set a correct target.
delete newContext.variables["target"];
return newContext;
});
if(options.as) {
result.forEach((resultContext)=>{
Action.setVariable(resultContext, options.as, conceptUUIDs);
});
}
}
VarvPerformance.stop("SelectAction.doSelect", mark);
if(DEBUG_SELECT) {
console.groupEnd();
}
return result;
}
let result = [];
let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
if(optionsWithArguments.forEach) {
//Individual mode
result = await this.forEachContext(contexts, actionArguments,async (context, options)=>{
return await doSelect(context, options, optionsWithArguments);
});
} else {
//Bulk mode
//Find any common variables and keep
let commonVariables = Action.getCommonVariables(contexts);
let commonTarget = -1;
contexts.forEach((context)=>{
if(commonTarget === -1) {
commonTarget = context.target;
} else {
if(commonTarget !== context.target) {
commonTarget = null;
}
}
});
let optionsWithVariablesAndArguments = await Action.lookupVariables(optionsWithArguments, {target: commonTarget, variables: commonVariables});
result = await doSelect({target: commonTarget, variables: commonVariables}, optionsWithVariablesAndArguments, optionsWithArguments);
}
if(optionsWithArguments.stopIfEmpty && result.length === 0) {
throw new StopError("Action '"+self.name+"' had no output, and 'stopIfEmpty' was set!");
}
return result;
}
}
Action.registerPrimitiveAction("select", SelectAction);
window.SelectAction = SelectAction;
/**
* An action 'storeSelection' that stores the current concept selection as a variable
* @memberOf ContextActions
* @example
* {
* "storeSelection": {
* "as": "mySelectionVariableName"
* }
* }
*
* @example
* //Shorthand
* {
* "storeSelection": "mySelectionVariableName"
* }
*/
class StoreSelectionAction extends Action {
static options() {
return {
"as": "@string"
};
}
constructor(name, options, concept) {
if(typeof options === "string") {
options = {
"as": options
};
}
super(name, options, concept);
}
async apply(contexts, actionArguments) {
let uuids = contexts.filter((context)=>{
return context.target != null;
}).map((context)=>{
return context.target;
});
let variableName = Action.defaultVariableName(this);
let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
let commonVariables = Action.getCommonVariables(contexts);
let optionsWithVariablesAndArguments = await Action.lookupVariables(optionsWithArguments, {variables: commonVariables});
if(optionsWithVariablesAndArguments.as) {
variableName = optionsWithVariablesAndArguments.as;
}
contexts.forEach((context)=>{
Action.setVariable(context, variableName, uuids.slice());
})
return contexts;
}
}
Action.registerPrimitiveAction("storeSelection", StoreSelectionAction);
window.StoreSelectionAction = StoreSelectionAction;
/**
* An action "limit" that limits the current selected concepts to a given count, starting from first or last
* @memberOf ContextActions
* @example
* //Limit concepts to 1, starting from first
* {
* "limit": {
* "count": 1,
* "last": false
* }
* }
*
* @example
* //Limit concepts to 1, starting from first (shorthand version)
* {
* "limit": 1
* }
*
* @example
* //Limit concepts to 2, starting from last
* {
* "limit": {
* "count": 2,
* "last": true
* }
* }
*/
class LimitAction extends Action {
static options() {
return {
"count": "number",
"last": "boolean%false"
};
}
constructor(name, options, concept) {
//Shorthand
if(typeof options === "number") {
options = {
count: options
}
}
super(name, options, concept);
}
async apply(contexts, actionArguments) {
return this.forEachContext(contexts, actionArguments, (context, options, index)=>{
if(options.count == null) {
throw new Error("Missing option 'count' on action 'limit'");
}
if(options.last === true) {
//Allow the last "count" elements through
if(index >= contexts.length - options.count) {
return context;
}
} else {
//Allow the first "count" elements through
if(index <= options.count-1) {
return context;
}
}
});
}
}
Action.registerPrimitiveAction("limit", LimitAction);
window.LimitAction = LimitAction;
/**
* An action "where" that filters on the currently selected concepts
*
* If the option "stopIfEmpty" is set to true, the action chain will terminate after the where action, if no concepts
* survived the filtering.
* <p>
* Available operators for property/variable/value filter:
* </p>
*
* <ul>
* <li>"equals" - "number", "boolean", "string", "concept", "array"</li>
* <li>"unequals" - "number", "boolean", "string", "concept", "array"</li>
* <li>"greaterThan" - "number", "string"</li>
* <li>"lessThan" - "number", "string"</li>
* <li>"greaterOrEquals" - "number", "string"</li>
* <li>"lessOrEquals" - "number", "string"</li>
* <li>"startsWith" - "string"</li>
* <li>"endsWith" - "string"</li>
* <li>"includes" - "string", "array"</li>
* <li>"includesAny" - "array"</li>
* <li>"includesAll" - "array"</li>
* <li>"matches" - "string"</li>
* </ul>
* @memberOf ContextActions
* @example
* {
* "where": {
* "or": [
* {
* "calculate": "10 + $myVariable$",
* "equals": 15
* },
* {
* "not": {
* "variable": "myVariableName",
* "equals": "myVariableValue"
* }
* },
* {
* "not": {
* "property": "myProperty",
* "equals": "myPropertyValue"
* }
* },
* {
* "and": [
* {
* "property": "myOtherProperty",
* "unequals": "somethingelse"
* },
* {
* "property": "myThirdProperty",
* "lowerThan": 10
* }
* ]
* }
* ]
* }
* }
*/
class FilterAction extends Action {
static options() {
return {
"$where": "filter",
"stopIfEmpty": "boolean%false"
};
}
constructor(name, options, concept) {
const defaultOptions = {
stopIfEmpty: false
};
options = Object.assign({}, defaultOptions, options);
super(name, options, concept);
}
async apply(contexts, actionArguments) {
const self = this;
let result = await this.forEachContext(contexts, actionArguments, async (context, options)=>{
let mark = VarvPerformance.start();
try {
let filter = FilterAction.constructFilter(options);
let shouldKeep = await filter.filter(context, this.concept);
VarvPerformance.stop("FilterAction.forEachContext.loop", mark);
if (shouldKeep) {
return context;
}
} catch(e) {
console.error(e);
}
return null;
});
let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
if(optionsWithArguments.stopIfEmpty && result.length === 0) {
throw new StopError("Action '"+self.name+"' had no output, and 'stopIfEmpty' was set!");
}
return result;
}
static constructFilter(options) {
let filter = FilterAction.constructFilterInternal(options);
filter.constructOptions = options;
return filter;
}
static constructFilterInternal(options) {
try {
let operator = null;
for(let op in FilterOps) {
if(typeof options[op] !== "undefined") {
operator = op;
break;
}
}
if (operator !== null) {
let value = options[operator];
if(operator === "hasProperty") {
return new FilterPropertyExists(value);
}
if(operator === "propertyType") {
return new FilterPropertyType(options.property, value);
}
if(options.calculation != null) {
//Property defined, this is a property filter
return new FilterCalc(options.calculation, operator, value);
}
if(options.property != null) {
//Property defined, this is a property filter
return new FilterProperty(options.property, operator, value);
}
if(options.variable != null) {
//Variable defined, this is a variable filter
return new FilterVariable(options.variable, operator, value);
}
return new FilterValue(operator, value);
} else {
//This should be an "and", "or", "not" or concept filter
if(options.concept != null) {
if(options.includeOthers != null) {
return new FilterConcept(options.concept, options.includeOthers);
}
//Concept defined, this is a concept filter
return new FilterConcept(options.concept);
}
if (options.or != null) {
let orFilters = [];
options.or.forEach((filterOptions) => {
orFilters.push(FilterAction.constructFilter(filterOptions));
});
return new FilterOr(orFilters);
} else if (options.and != null) {
let andFilters = [];
options.and.forEach((filterOptions) => {
andFilters.push(FilterAction.constructFilter(filterOptions));
});
return new FilterAnd(andFilters);
} else if (options.not != null) {
return new FilterNot(FilterAction.constructFilter(options.not));
}
}
} catch(e) {
console.error(e);
}
console.warn("No filter constructed using:", options);
return null;
}
}
Action.registerPrimitiveAction("where", FilterAction);
window.FilterAction = FilterAction;
/**
* An action "new" that creates a new concept, optionally setting properties on it as well.
* Runs in bulk mode unless 'forEach: true' is set. In bulk mode only 1 new concept is created, else 1 new would be created for every currently selected concept.
* 'select: false' is not supported in bulk mode ('forEach: false')
*
*
* @memberOf ContextActions
* @example
* {
* "new": {
* "concept": "myConcept",
* "with": {
* "myFirstProperty": "someValue",
* "mySecondProperty": false
* "myThirdProperty": "$myVariableName"
* }
* }
* }
*
* @example
* //Run in non-bulk mode, creates 1 "myConcept" for each currently selected concept
* {
* "new": {
* "concept": "myConcept",
* "forEach": true
* }
* }
*
* @example
* //Same as other example, but don't change the current selection, which means that the newly created concept is only passed along as a variable
* {
* "new": {
* "concept": "myConcept",
* "with": {
* "myFirstProperty": "someValue",
* "mySecondProperty": false
* "myThirdProperty": "$myVariableName"
* },
* "as": "myVariableName",
* "select": false
* }
* }
*/
class NewAction extends Action {
static options() {
return {
"concept": "string",
"with": "propertyList",
"as": "@string",
"select": "boolean%true"
}
}
constructor(name, options, concept) {
//Shorthand
if(typeof options === "string") {
options = {
concept: options
}
}
const defaultOptions = {
select: true,
forEach: false
};
super(name, Object.assign({}, defaultOptions, options), concept);
}
async apply(contexts, actionArguments) {
const self = this;
async function doNew(context, options) {
let concept = VarvEngine.getConceptFromType(options.concept);
let uuid = await concept.create(null, options.with);
let variableName = Action.defaultVariableName(self);
if (options.as != null) {
variableName = options.as;
}
Action.setVariable(context, variableName, uuid);
let select = options.select;
if(options.forEach == false) {
if(!select) {
console.warn("Uncompatible options for 'new' action - select: false and forEach: false. Bulk mode always selects the newly created instance")
}
select = true;
}
if(select) {
context.target = uuid;
}
return context;
}
let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
let result = [];
if(optionsWithArguments.forEach) {
result = await this.forEachContext(contexts, actionArguments, async (context, options) => {
return await doNew(context, options);
});
} else {
//Bulk mode
//Find any common variables and keep
let commonVariables = Action.getCommonVariables(contexts);
let optionsWithVariablesAndArguments = await Action.lookupVariables(optionsWithArguments, {variables: commonVariables});
result = [await doNew({variables: commonVariables}, optionsWithVariablesAndArguments)];
}
return result;
}
}
Action.registerPrimitiveAction("new", NewAction);
window.NewAction = NewAction;
/**
* An action "remove" that removes instances of concepts
* @memberOf ContextActions
* @example
* //Remove the current context target
* {
* "remove"
* }
*
* @example
* // Remove the concept or concepts (if variable points to an array) that the variable holds
* {
* "remove": "$someVariable"
* }
*/
class RemoveAction extends Action {
static options() {
return {
"target": "@string"
};
}
constructor(name, options, concept) {
if(typeof options === "string") {
options = {
target: options
}
}
super(name, options, concept);
}
async apply(contexts, actionArguments) {
return this.forEachContext(contexts, actionArguments, async (context, options)=>{
let removeUuids = options.target;
if(removeUuids == null) {
if(context.target == null) {
throw new Error("No uuid's supplied to be removed, and context.target is non existant");
}
//No remove option specified, remove current target
removeUuids = context.target;
context.target = null;
}
if(!Array.isArray(removeUuids)) {
removeUuids = [removeUuids];
}
for(let uuid of removeUuids) {
let concept = await VarvEngine.getConceptFromUUID(uuid);
await concept.delete(uuid);
}
//Return null, to signal that this target/context is now invalid.
return null;
});
}
}
Action.registerPrimitiveAction("remove", RemoveAction);
window.RemoveAction = RemoveAction;
/**
* An action "eval" that takes a filter, and sets a variable to true/false, depending on if the filter matched or not
* @memberOf ContextActions
* @example
* {
* "eval": {
* "and": [
* { "property": "myFirstProperty", "equals": false },
* { "property": "mySecondProperty", "equals": false }
* ]
* }
* }
*/
class EvalAction extends Action {
static options() {
return {
"$eval": "filter",
"as": "@string"
};
}
constructor(name, options, concept) {
super(name, options, concept);
}
async apply(contexts, actionArguments) {
const self = this;
return this.forEachContext(contexts, actionArguments, async (context, options)=>{
let filter = FilterAction.constructFilter(options);
let shouldFilter = await filter.filter(context);
let variableName = Action.defaultVariableName(self);
if(options.as != null) {
variableName = options.as;
}
Action.setVariable(context, variableName, shouldFilter);
return context;
});
}
}
Action.registerPrimitiveAction("eval", EvalAction);
window.EvalAction = EvalAction;
/**
* An action "count" that counts how many concepts exists with the given where filter. Sets a variable to the count
* @memberOf ContextActions
* @example
* // Counts how many "myConcept" that matches the given "where" filter, and saves the result as "myResultVariableName"
* {
* "count": {
* "concept": "myConcept",
* "where": {
* "property": "myProperty",
* "equals": "myTestValue"
* },
* "as": "myResultVariableName"
* }
* }
*
* @example
* // Counts how many "myConcept" there is, and saves the result as "count"
* {
* "count": "myConcept
* }
*/
class CountAction extends SelectAction {
static options() {
return {
"$selectType": "enumValue[concept]",
"where": "filter",
"as": "@string",
"forEach": "boolean%false"
};
}
constructor(name, options, concept) {
if(typeof options === "string") {
options = {
concept: options
}
}
super(name, options, concept);
}
async apply(contexts, actionArguments) {
const self = this;
let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
async function doCount(context, options) {
if(options.concept == null) {
throw new Error("Count requires an option 'concept'");
}
let filter = null;
if(self.wherePart) {
let clonedVariables = Object.assign({}, context.variables);
let filterContext = {target: null, lastTarget: context.target, variables: clonedVariables};
let lookupWhereWithArguments = await Action.lookupArguments(self.wherePart, actionArguments);
let lookupWhereOptions = await Action.lookupVariables(lookupWhereWithArguments, filterContext);
filter = await FilterAction.constructFilter(lookupWhereOptions);
}
return await VarvEngine.countInstances(VarvEngine.getAllImplementingConceptNames(options.concept), filter, context, 0, self.concept);
}
if(optionsWithArguments.forEach) {
return this.forEachContext(contexts, actionArguments, async (context, options)=>{
let clonedContext = Action.cloneContext(context);
let count = await doCount(clonedContext, options);
let variableName = Action.defaultVariableName(self);
if(options.as != null) {
variableName = options.as;
}
Action.setVariable(context, variableName, count);
return context;
});
} else {
//Bulk mode
//Find any common variables and keep
let commonVariables = Action.getCommonVariables(contexts);
let commonTarget = -1;
contexts.forEach((context)=>{
if(commonTarget === -1) {
commonTarget = context.target;
} else {
if(commonTarget !== context.target) {
commonTarget = null;
}
}
});
let optionsWithVariablesAndArguments = await Action.lookupVariables(optionsWithArguments, {target: commonTarget, variables: commonVariables});
let count = await doCount({target: commonTarget, variables: commonVariables}, optionsWithVariablesAndArguments);
return this.forEachContext(contexts, actionArguments, async (context, options)=>{
let variableName = Action.defaultVariableName(self);
if(options.as != null) {
variableName = options.as;
}
Action.setVariable(context, variableName, count);
return context;
});
}
}
}
Action.registerPrimitiveAction("count", CountAction);
window.CountAction = CountAction;
/**
* An action "exists" that checks if any concepts exists with the given where filter. Sets a variable to true/false depending.
* @memberOf ContextActions
* @example
* // Sets a variable "myResultVariableName" to true/false depending on if any "myConcept" that matches the where filter exists
* {
* "exists": {
* "concept": "myConcept",
* "where": {
* "property": "myProperty",
* "equals": "myTestValue"
* },
* "as": "myResultVariableName"
* }
* }
*
* @example
* // Sets a variable "exists" to true/false depending on if any "myConcept" exists
* {
* "exists": "myConcept"
* }
*/
class ExistsAction extends SelectAction {
static options() {
return {
"$selectType": "enumValue[concept]",
"where": "filter",
"as": "@string",
"forEach": "boolean%false"
};
}
constructor(name, options, concept) {
if(typeof options === "string") {
options = {
concept: options
}
}
super(name, options, concept);
}
async apply(contexts, actionArguments) {
const self = this;
let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
async function doExists(context, options) {
if(options.concept == null) {
throw new Error("Exists requires an option 'concept'")
}
let filter = null;
if(self.wherePart) {
let clonedVariables = Object.assign({}, context.variables);
let filterContext = {target: null, lastTarget: context.target, variables: clonedVariables};
let lookupWhereWithArguments = await Action.lookupArguments(self.wherePart, actionArguments);
let lookupWhereOptions = await Action.lookupVariables(lookupWhereWithArguments, filterContext);
filter = await FilterAction.constructFilter(lookupWhereOptions);
}
return await VarvEngine.existsInstance(VarvEngine.getAllImplementingConceptNames(options.concept), filter, context, 1, self.concept);
}
if(optionsWithArguments.forEach) {
return this.forEachContext(contexts, actionArguments, async (context, options)=>{
let clonedContext = Action.cloneContext(context);
let exists = await doExists(clonedContext, options);
let variableName = Action.defaultVariableName(self);
if(options.as != null) {
variableName = options.as;
}
Action.setVariable(context, variableName, exists);
return context;
});
} else {
//Bulk mode
//Find any common variables and keep
let commonVariables = Action.getCommonVariables(contexts);
let commonTarget = -1;
contexts.forEach((context)=>{
if(commonTarget === -1) {
commonTarget = context.target;
} else {
if(commonTarget !== context.target) {
commonTarget = null;
}
}
});
let optionsWithVariablesAndArguments = await Action.lookupVariables(optionsWithArguments, {target: commonTarget, variables: commonVariables});
let exists = await doExists({target: commonTarget, variables: commonVariables}, optionsWithVariablesAndArguments);
return this.forEachContext(contexts, actionArguments, async (context, options)=>{
let variableName = Action.defaultVariableName(self);
if(options.as != null) {
variableName = options.as;
}
Action.setVariable(context, variableName, exists);
return context;
});
}
}
}
Action.registerPrimitiveAction("exists", ExistsAction);
window.ExistsAction = ExistsAction;
/**
* An action 'sort' that sorts the selected concepts naturally based on a property/variable, can be sorted either ascending or descending.
*
* Always sorts "naturally", and only supports string, number and boolean types.
* @memberOf ContextActions
* @example
* {"sort": {
* "property": "myProperty",
* "order": "asc"
* }}
*
* @example
* {"sort": {
* "variable": "myVariable",
* "order": "desc"
* }}
*
* @example
* //Shorthand sorts ascending on property
* {"sort": "myProperty"}
*/
class SortAction extends Action {
constructor(name, options, concept) {
if(typeof options === "string") {
options = {
"property": options
}
}
if(options.order == null) {
options.order = "asc"
}
super(name, options, concept);
}
async apply(contexts, actionArguments) {
const self = this;
let optionsWithArguments = await Action.lookupArguments(this.options, actionArguments);
if(optionsWithArguments.property == null && optionsWithArguments.variable == null) {
throw new Error("Missing option property or variable on sort action");
}
const sortedContexts = await Promise.all(contexts.map(async (context)=> {
let obj = {
c: context,
t: await VarvEngine.getConceptFromUUID(context.target),
};
if(optionsWithArguments.property) {
//We have an invariant that says that all selected concepts are of same type
console.warn("TODO: Implement some fix for polymorphism enabled sort");
const concept = obj.t;
if(concept == null) {
throw new Error("Unable to find concept for uuid ["+obj.c.target+"]");
}
const property = concept.getProperty(optionsWithArguments.property);
if(property == null) {
throw new Error("Unable to find property ["+optionsWithArguments.property+"] on ["+concept.name+"]");
}
let value = await property.getValue(obj.c.target);
obj.v = value;
} else {
//Variable
let value = Action.getVariable(obj.c, optionsWithArguments.variable);
obj.v = value;
}
return obj;
}));
sortedContexts.sort((c1, c2)=>{
let s1 = c1.v;
let s2 = c2.v;
if(typeof s1 !== typeof s2) {
throw new Error("Unable to sort when not the same type: ("+s1+") - ("+s2+")");
}
if(typeof s1 === "number") {
return s1 - s2;
} else if(typeof s1 === "string") {
return s1.localeCompare(s2);
} else if(typeof s1 === "boolean") {
return s1 - s2;
} else {
console.warn("Unable to sort "+(typeof s1));
}
});
return sortedContexts.map((o)=>{
return o.c;
});
}
}
Action.registerPrimitiveAction("sort", SortAction);
window.SortAction = SortAction;
/**
* An action "clone" that copies instances of concepts
* @memberOf ContextActions
* @example
* // Clone the current context target
* {
* "clone"
* }
*
* @example
* // Clone the concepts referenced in $myUuidArray
* {"clone": {
* "of": "$myUuidArray"
* }}
*
* @example
* //Do a deep clone of current context target, setting the variable "myVariable" to the result, not selecting the new clone
* {"clone: {
* "deep": true,
* "select": false,
* "as": "myVariable"
* }}
*
*/
class CloneAction extends Action {
static options() {
return {
"target": "@string"
};
}
constructor(name, options, concept) {
if(typeof options === "string") {
options = {
of: options
}
}
const defaultOptions = {
select: true,
deep: false
};
super(name, Object.assign({}, defaultOptions, options), concept);
}
async apply(contexts, actionArguments) {
const self = this;
return this.forEachContext(contexts, actionArguments, async (context, options)=>{
let cloneUUIDs = options.of;
if(cloneUUIDs == null) {
if(context.target == null) {
throw new Error("No uuid's in 'of' option supplied to be cloned, and context.target is non existant");
}
// No clone option specified, clone current target
cloneUUIDs = context.target;
}
if(!Array.isArray(cloneUUIDs)) {
cloneUUIDs = [cloneUUIDs];
}
let resultingContexts = [];
let newUUIDs = [];
for(let uuid of cloneUUIDs) {
let concept = await VarvEngine.getConceptFromUUID(uuid);
let clone = await concept.clone(uuid, options.deep);
newUUIDs.push(clone);
}
// Handle "as" before creating result contexts as it needs to be on all of them.
let variableName = Action.defaultVariableName(self);
if (options.as != null) {
variableName = options.as;
}
if(options.select) {
//Select the new clones
for(let uuid of newUUIDs) {
let resultContext = Action.cloneContext(context);
resultContext.target = uuid;
let variableValue = newUUIDs;
if(newUUIDs.length === 1) {
variableValue = uuid;
}
Action.setVariable(resultContext, variableName, variableValue);
resultingContexts.push(resultContext);
}
return resultingContexts;
} else {
//Keep our old context selected
if(newUUIDs.length === 1) {
newUUIDs = newUUIDs[0];
}
Action.setVariable(context, variableName, newUUIDs);
return context;
}
});
}
}
Action.registerPrimitiveAction("clone", CloneAction);
window.CloneAction = CloneAction;
/**
* An action 'setType' that can change the type of a concept instance, highly experimental
*
* @memberOf ContextActions
*
* @example
* //Changes the type of all currently selected concept instances to "myNewConcept"
* {
* "setType": {"concept": "myNewConcept"}
* }
*
* @example
* //Shorthand
* {"setType": "myNewConcept"}
*/
class SetTypeAction extends Action {
constructor(name, options, concept) {
if(typeof options === "string") {
options = {
concept: options
}
}
super(name, options, concept);
}
async apply(contexts, actionArguments) {
const self = this;
return this.forEachContext(contexts, actionArguments, async (context, options) => {
if(options.concept == null) {
throw new Error("Missing concept in action 'setType'");
}
let targetConcept = VarvEngine.getConceptFromType(options.concept);
if(targetConcept == null) {
throw new Error("Unknown concept \""+options.concept+"\" in action 'setType'");
}
if(context.target != null) {
let concept = await VarvEngine.getConceptFromUUID(context.target);
if(concept != null) {
await VarvEngine.switchConceptType(context.target, targetConcept, concept);
}
} else {
throw new Error("Missing context.target in action 'setType'");
}
return context;
});
}
}
Action.registerPrimitiveAction("setType", SetTypeAction);
window.SetTypeAction = SetTypeAction;