mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-11 14:52:54 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
21
packages/noodl-runtime/LICENSE
Normal file
21
packages/noodl-runtime/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Future Platforms AB
|
||||
|
||||
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.
|
||||
420
packages/noodl-runtime/noodl-runtime.js
Normal file
420
packages/noodl-runtime/noodl-runtime.js
Normal file
@@ -0,0 +1,420 @@
|
||||
'use strict';
|
||||
|
||||
const NodeContext = require('./src/nodecontext');
|
||||
const EditorConnection = require('./src/editorconnection');
|
||||
const generateNodeLibrary = require('./src/nodelibraryexport');
|
||||
const ProjectSettings = require('./src/projectsettings');
|
||||
const GraphModel = require('./src/models/graphmodel');
|
||||
const NodeDefinition = require('./src/nodedefinition');
|
||||
const Node = require('./src/node');
|
||||
const EditorModelEventsHandler = require('./src/editormodeleventshandler');
|
||||
const Services = require('./src/services/services');
|
||||
const EdgeTriggeredInput = require('./src/edgetriggeredinput');
|
||||
|
||||
const EventEmitter = require('./src/events');
|
||||
const asyncPool = require('./src/async-pool');
|
||||
|
||||
function registerNodes(noodlRuntime) {
|
||||
[
|
||||
require('./src/nodes/componentinputs'),
|
||||
require('./src/nodes/componentoutputs'),
|
||||
|
||||
require('./src/nodes/std-library/runtasks'),
|
||||
|
||||
// Data
|
||||
require('./src/nodes/std-library/data/restnode'),
|
||||
|
||||
// Custom code
|
||||
require('./src/nodes/std-library/expression'),
|
||||
require('./src/nodes/std-library/simplejavascript'),
|
||||
|
||||
// Records
|
||||
require('./src/nodes/std-library/data/dbcollectionnode2'),
|
||||
require('./src/nodes/std-library/data/dbmodelnode2'),
|
||||
require('./src/nodes/std-library/data/setdbmodelpropertiesnode'),
|
||||
require('./src/nodes/std-library/data/deletedbmodelpropertiesnode'),
|
||||
require('./src/nodes/std-library/data/newdbmodelpropertiesnode'),
|
||||
require('./src/nodes/std-library/data/dbmodelnode-addrelation'),
|
||||
require('./src/nodes/std-library/data/dbmodelnode-removerelation'),
|
||||
require('./src/nodes/std-library/data/filterdbmodelsnode'),
|
||||
|
||||
// Object
|
||||
require('./src/nodes/std-library/data/modelnode2'),
|
||||
require('./src/nodes/std-library/data/setmodelpropertiesnode'),
|
||||
require('./src/nodes/std-library/data/newmodelnode'),
|
||||
|
||||
// Cloud
|
||||
require('./src/nodes/std-library/data/cloudfilenode'),
|
||||
require('./src/nodes/std-library/data/dbconfig'),
|
||||
|
||||
// Variables
|
||||
require('./src/nodes/std-library/variables/number'),
|
||||
require('./src/nodes/std-library/variables/string'),
|
||||
require('./src/nodes/std-library/variables/boolean'),
|
||||
|
||||
// Utils
|
||||
require('./src/nodes/std-library/condition'),
|
||||
require('./src/nodes/std-library/and'),
|
||||
require('./src/nodes/std-library/or'),
|
||||
require('./src/nodes/std-library/booleantostring'),
|
||||
require('./src/nodes/std-library/datetostring'),
|
||||
require('./src/nodes/std-library/stringmapper'),
|
||||
require('./src/nodes/std-library/inverter'),
|
||||
require('./src/nodes/std-library/substring'),
|
||||
require('./src/nodes/std-library/stringformat'),
|
||||
require('./src/nodes/std-library/counter'),
|
||||
require('./src/nodes/std-library/uniqueid'),
|
||||
|
||||
// User
|
||||
require('./src/nodes/std-library/user/setuserproperties'),
|
||||
require('./src/nodes/std-library/user/user')
|
||||
].forEach((node) => noodlRuntime.registerNode(node));
|
||||
}
|
||||
|
||||
function NoodlRuntime(args) {
|
||||
args = args || {};
|
||||
args.platform = args.platform || {};
|
||||
NoodlRuntime.instance = this;
|
||||
|
||||
this.type = args.type || 'browser';
|
||||
this.noodlModules = [];
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.updateScheduled = false;
|
||||
this.rootComponent = null;
|
||||
this._currentLoadedData = null;
|
||||
this.isWaitingForExport = true;
|
||||
this.graphModel = new GraphModel();
|
||||
this.errorHandlers = [];
|
||||
this.frameNumber = 0;
|
||||
this.dontCreateRootComponent = !!args.dontCreateRootComponent;
|
||||
this.componentFilter = args.componentFilter;
|
||||
|
||||
this.runningInEditor = args.runDeployed ? false : true;
|
||||
|
||||
this.platform = {
|
||||
requestUpdate: args.platform.requestUpdate,
|
||||
getCurrentTime: args.platform.getCurrentTime,
|
||||
webSocketOptions: args.platform.webSocketOptions,
|
||||
objectToString: args.platform.objectToString
|
||||
};
|
||||
|
||||
if (!args.platform.requestUpdate) {
|
||||
throw new Error('platform.requestUpdate must be set');
|
||||
}
|
||||
|
||||
if (!args.platform.getCurrentTime) {
|
||||
throw new Error('platform.getCurrentTime must be set');
|
||||
}
|
||||
|
||||
//Create an editor connection even if we're running deployed.
|
||||
//If won't connect and act as a "noop" in deployed mode,
|
||||
// and reduce the need for lots of if(editorConnection)
|
||||
this.editorConnection = new EditorConnection({
|
||||
platform: args.platform,
|
||||
runtimeType: this.type
|
||||
});
|
||||
|
||||
this.context = new NodeContext({
|
||||
runningInEditor: args.runDeployed ? false : true,
|
||||
editorConnection: this.editorConnection,
|
||||
platform: this.platform,
|
||||
graphModel: this.graphModel
|
||||
});
|
||||
|
||||
this.context.eventEmitter.on('scheduleUpdate', this.scheduleUpdate.bind(this));
|
||||
|
||||
if (!args.runDeployed) {
|
||||
this._setupEditorCommunication(args);
|
||||
}
|
||||
|
||||
this.registerGraphModelListeners();
|
||||
|
||||
registerNodes(this);
|
||||
}
|
||||
|
||||
NoodlRuntime.prototype.prefetchBundles = async function (bundleNames, numParallelFetches) {
|
||||
await asyncPool(numParallelFetches, bundleNames, async (name) => {
|
||||
await this.context.fetchComponentBundle(name);
|
||||
});
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype._setupEditorCommunication = function (args) {
|
||||
function objectEquals(x, y) {
|
||||
if (x === null || x === undefined || y === null || y === undefined) {
|
||||
return x === y;
|
||||
}
|
||||
if (x === y) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(x) && x.length !== y.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if they are strictly equal, they both need to be object at least
|
||||
if (!(x instanceof Object)) {
|
||||
return false;
|
||||
}
|
||||
if (!(y instanceof Object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// recursive object equality check
|
||||
var p = Object.keys(x);
|
||||
return (
|
||||
Object.keys(y).every(function (i) {
|
||||
return p.indexOf(i) !== -1;
|
||||
}) &&
|
||||
p.every(function (i) {
|
||||
return objectEquals(x[i], y[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.editorConnection.on('exportDataFull', async (exportData) => {
|
||||
if (this.graphModel.isEmpty() === false) {
|
||||
this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isWaitingForExport = false;
|
||||
if (objectEquals(this._currentLoadedData, exportData) === false) {
|
||||
if (this.componentFilter) {
|
||||
exportData.components = exportData.components.filter((c) => this.componentFilter(c));
|
||||
}
|
||||
|
||||
await this.setData(exportData);
|
||||
|
||||
//get the rest of the components
|
||||
//important to get all the dynamic ports evaluated
|
||||
if (exportData.componentIndex) {
|
||||
const allBundles = Object.keys(exportData.componentIndex);
|
||||
await this.prefetchBundles(allBundles, 2);
|
||||
}
|
||||
|
||||
this.graphModel.emit('editorImportComplete');
|
||||
}
|
||||
});
|
||||
|
||||
this.editorConnection.on('reload', this.reload.bind(this));
|
||||
this.editorConnection.on('modelUpdate', this.onModelUpdateReceived.bind(this));
|
||||
this.editorConnection.on('metadataUpdate', this.onMetaDataUpdateReceived.bind(this));
|
||||
|
||||
this.editorConnection.on('connected', () => {
|
||||
this.sendNodeLibrary();
|
||||
});
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.setDebugInspectorsEnabled = function (enabled) {
|
||||
this.context.setDebugInspectorsEnabled(enabled);
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.registerModule = function (module) {
|
||||
if (module.nodes) {
|
||||
for (let nodeDefinition of module.nodes) {
|
||||
if (!nodeDefinition.node) nodeDefinition = { node: nodeDefinition };
|
||||
nodeDefinition.node.module = module.name || 'Unknown Module';
|
||||
this.registerNode(nodeDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
this.noodlModules.push(module);
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.registerGraphModelListeners = function () {
|
||||
var self = this;
|
||||
|
||||
this.graphModel.on(
|
||||
'componentAdded',
|
||||
function (component) {
|
||||
self.context.registerComponentModel(component);
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
this.graphModel.on(
|
||||
'componentRemoved',
|
||||
function (component) {
|
||||
self.context.deregisterComponentModel(component);
|
||||
},
|
||||
this
|
||||
);
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.reload = function () {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.registerNode = function (nodeDefinition) {
|
||||
if (nodeDefinition.node) {
|
||||
const definedNode = NodeDefinition.defineNode(nodeDefinition.node);
|
||||
this.context.nodeRegister.register(definedNode);
|
||||
|
||||
definedNode.setupNumberedInputDynamicPorts &&
|
||||
definedNode.setupNumberedInputDynamicPorts(this.context, this.graphModel);
|
||||
} else {
|
||||
this.context.nodeRegister.register(nodeDefinition);
|
||||
}
|
||||
|
||||
nodeDefinition.setup && nodeDefinition.setup(this.context, this.graphModel);
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype._setRootComponent = async function (rootComponentName) {
|
||||
if (this.rootComponent && this.rootComponent.name === rootComponentName) return;
|
||||
|
||||
if (this.rootComponent) {
|
||||
this.rootComponent.model && this.rootComponent.model.removeListenersWithRef(this);
|
||||
this.rootComponent = undefined;
|
||||
}
|
||||
|
||||
if (rootComponentName) {
|
||||
this.rootComponent = await this.context.createComponentInstanceNode(rootComponentName, 'rootComponent');
|
||||
|
||||
this.rootComponent.componentModel.on('rootAdded', () => this.eventEmitter.emit('rootComponentUpdated'), this);
|
||||
this.rootComponent.componentModel.on('rootRemoved', () => this.eventEmitter.emit('rootComponentUpdated'), this);
|
||||
|
||||
this.context.setRootComponent(this.rootComponent);
|
||||
}
|
||||
|
||||
this.eventEmitter.emit('rootComponentUpdated');
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.setData = async function (graphData) {
|
||||
// Added for SSR Support
|
||||
// In SSR, we re-load the graphData and when we render the componet it will
|
||||
// invoke this method again, which will cause a duplicate node exception.
|
||||
// To avoid this, we flag the runtime to not load again.
|
||||
if (this._disableLoad) return;
|
||||
|
||||
this._currentLoadedData = graphData;
|
||||
await this.graphModel.importEditorData(graphData);
|
||||
|
||||
// Run setup on all modules
|
||||
for (const module of this.noodlModules) {
|
||||
typeof module.setup === 'function' && module.setup.apply(module);
|
||||
}
|
||||
|
||||
if (this.dontCreateRootComponent !== true) {
|
||||
await this._setRootComponent(this.graphModel.rootComponent);
|
||||
|
||||
//listen to delta updates on the root component
|
||||
this.graphModel.on('rootComponentNameUpdated', (name) => {
|
||||
this._setRootComponent(name);
|
||||
});
|
||||
|
||||
//check if the root component was deleted
|
||||
this.graphModel.on('componentRemoved', (componentModel) => {
|
||||
if (this.rootComponent && this.rootComponent.name === componentModel.name) {
|
||||
this._setRootComponent(null);
|
||||
}
|
||||
});
|
||||
|
||||
//check if the root component was added when it previously didn't exist (e.g. when user deletes it and then hits undo)
|
||||
this.graphModel.on('componentAdded', (componentModel) => {
|
||||
setTimeout(() => {
|
||||
if (!this.rootComponent && this.graphModel.rootComponent === componentModel.name) {
|
||||
this._setRootComponent(componentModel.name);
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
}
|
||||
|
||||
this.scheduleUpdate();
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.scheduleUpdate = function () {
|
||||
if (this.updateScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateScheduled = true;
|
||||
this.platform.requestUpdate(NoodlRuntime.prototype._doUpdate.bind(this));
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype._doUpdate = function () {
|
||||
this.updateScheduled = false;
|
||||
|
||||
this.context.currentFrameTime = this.platform.getCurrentTime();
|
||||
|
||||
this.context.eventEmitter.emit('frameStart');
|
||||
|
||||
this.context.update();
|
||||
|
||||
this.context.eventEmitter.emit('frameEnd');
|
||||
|
||||
this.frameNumber++;
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.setProjectSettings = function (settings) {
|
||||
this.projectSettings = settings;
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.getNodeLibrary = function () {
|
||||
var projectSettings = ProjectSettings.generateProjectSettings(this.graphModel.getSettings(), this.noodlModules);
|
||||
|
||||
if (this.projectSettings) {
|
||||
this.projectSettings.ports && (projectSettings.ports = projectSettings.ports.concat(this.projectSettings.ports));
|
||||
this.projectSettings.dynamicports &&
|
||||
(projectSettings.dynamicports = projectSettings.ports.concat(this.projectSettings.dynamicports));
|
||||
}
|
||||
|
||||
var nodeLibrary = generateNodeLibrary(this.context.nodeRegister);
|
||||
nodeLibrary.projectsettings = projectSettings;
|
||||
return JSON.stringify(nodeLibrary, null, 3);
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.sendNodeLibrary = function () {
|
||||
const nodeLibrary = this.getNodeLibrary();
|
||||
if (this.lastSentNodeLibrary !== nodeLibrary) {
|
||||
this.lastSentNodeLibrary = nodeLibrary;
|
||||
this.editorConnection.sendNodeLibrary(nodeLibrary);
|
||||
}
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.connectToEditor = function (address) {
|
||||
this.editorConnection.connect(address);
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.onMetaDataUpdateReceived = function (event) {
|
||||
if (!this.graphModel.isEmpty()) {
|
||||
EditorMetaDataEventsHandler.handleEvent(this.context, this.graphModel, event);
|
||||
}
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.onModelUpdateReceived = async function (event) {
|
||||
if (this.isWaitingForExport) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'projectInstanceChanged') {
|
||||
this.reload();
|
||||
}
|
||||
//wait for data to load before applying model changes
|
||||
else if (this.graphModel.isEmpty() === false) {
|
||||
await EditorModelEventsHandler.handleEvent(this.context, this.graphModel, event);
|
||||
}
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.addErrorHandler = function (callback) {
|
||||
this.errorHandlers.push(callback);
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.reportError = function (message) {
|
||||
this.errorHandlers.forEach(function (eh) {
|
||||
eh(message);
|
||||
});
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.getProjectSettings = function () {
|
||||
return this.graphModel.getSettings();
|
||||
};
|
||||
|
||||
NoodlRuntime.prototype.getMetaData = function (key) {
|
||||
return this.graphModel.getMetaData(key);
|
||||
};
|
||||
|
||||
NoodlRuntime.Services = Services;
|
||||
NoodlRuntime.Node = Node;
|
||||
NoodlRuntime.NodeDefinition = NodeDefinition;
|
||||
NoodlRuntime.EdgeTriggeredInput = EdgeTriggeredInput;
|
||||
|
||||
module.exports = NoodlRuntime;
|
||||
15
packages/noodl-runtime/package.json
Normal file
15
packages/noodl-runtime/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@noodl/runtime",
|
||||
"version": "2.7.0",
|
||||
"main": "noodl-runtime.js",
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^28.1.0"
|
||||
}
|
||||
}
|
||||
20
packages/noodl-runtime/src/api/cloudfile.js
Normal file
20
packages/noodl-runtime/src/api/cloudfile.js
Normal file
@@ -0,0 +1,20 @@
|
||||
class CloudFile {
|
||||
constructor({ name, url }) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.url;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CloudFile;
|
||||
613
packages/noodl-runtime/src/api/cloudstore.js
Normal file
613
packages/noodl-runtime/src/api/cloudstore.js
Normal file
@@ -0,0 +1,613 @@
|
||||
const NoodlRuntime = require('../../noodl-runtime');
|
||||
const Model = require('../model');
|
||||
const Collection = require('../collection');
|
||||
const CloudFile = require('./cloudfile');
|
||||
const EventEmitter = require('../events');
|
||||
|
||||
const _protectedFields = {
|
||||
_common: ['_createdAt', '_updatedAt', 'objectId'],
|
||||
_User: ['_email_verify_token']
|
||||
};
|
||||
|
||||
function _removeProtectedFields(data, className) {
|
||||
const _data = Object.assign({}, data);
|
||||
_protectedFields._common.forEach((f) => delete _data[f]);
|
||||
if (className && _protectedFields[className]) _protectedFields[className].forEach((f) => delete _data[f]);
|
||||
|
||||
return _data;
|
||||
}
|
||||
|
||||
class CloudStore {
|
||||
constructor(modelScope) {
|
||||
this._initCloudServices();
|
||||
|
||||
this.events = new EventEmitter();
|
||||
this.events.setMaxListeners(10000);
|
||||
this.modelScope = modelScope;
|
||||
|
||||
this._fromJSON = (item, collectionName) => CloudStore._fromJSON(item, collectionName, modelScope);
|
||||
this._deserializeJSON = (data, type) => CloudStore._deserializeJSON(data, type, modelScope);
|
||||
this._serializeObject = (data, collectionName) => CloudStore._serializeObject(data, collectionName, modelScope);
|
||||
}
|
||||
|
||||
_initCloudServices() {
|
||||
_collections = undefined; // clear collection cache, so it's refetched
|
||||
const cloudServices = NoodlRuntime.instance.getMetaData('cloudservices');
|
||||
|
||||
if (cloudServices) {
|
||||
this.appId = cloudServices.appId;
|
||||
this.endpoint = cloudServices.endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
on() {
|
||||
this.events.on.apply(this.events, arguments);
|
||||
}
|
||||
|
||||
off() {
|
||||
this.events.off.apply(this.events, arguments);
|
||||
}
|
||||
|
||||
_makeRequest(path, options) {
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// Running in browser
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
var json;
|
||||
try {
|
||||
// In SSR, we dont have xhr.response
|
||||
json = JSON.parse(xhr.response || xhr.responseText);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr.status === 200 || xhr.status === 201) {
|
||||
options.success(json);
|
||||
} else options.error(json || { error: xhr.responseText, status: xhr.status });
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open(options.method || 'GET', this.endpoint + path, true);
|
||||
|
||||
xhr.setRequestHeader('X-Parse-Application-Id', this.appId);
|
||||
if (typeof _noodl_cloudservices !== 'undefined')
|
||||
xhr.setRequestHeader('X-Parse-Master-Key', _noodl_cloudservices.masterKey);
|
||||
|
||||
// Check for current users
|
||||
var _cu = localStorage['Parse/' + this.appId + '/currentUser'];
|
||||
if (_cu !== undefined) {
|
||||
try {
|
||||
const currentUser = JSON.parse(_cu);
|
||||
xhr.setRequestHeader('X-Parse-Session-Token', currentUser.sessionToken);
|
||||
} catch (e) {
|
||||
// Failed to extract session token
|
||||
}
|
||||
}
|
||||
|
||||
if (options.onUploadProgress) {
|
||||
xhr.upload.onprogress = (pe) => options.onUploadProgress(pe);
|
||||
}
|
||||
|
||||
if (options.content instanceof File) {
|
||||
xhr.send(options.content);
|
||||
} else {
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify(options.content));
|
||||
}
|
||||
} else {
|
||||
// Running in cloud runtime
|
||||
const endpoint = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.endpoint : this.endpoint;
|
||||
const appId = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.appId : this.appId;
|
||||
const masterKey = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.masterKey : undefined;
|
||||
|
||||
fetch(endpoint + path, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': appId,
|
||||
'X-Parse-Master-Key': masterKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(options.content)
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.status === 200 || r.status === 201) {
|
||||
if (options.method === 'DELETE') {
|
||||
options.success(undefined);
|
||||
} else {
|
||||
r.json()
|
||||
.then((json) => options.success(json))
|
||||
.catch((e) =>
|
||||
options.error({
|
||||
error: 'CloudStore: Failed to get json result.'
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (options.method === 'DELETE') {
|
||||
options.error({ error: 'Failed to delete.' });
|
||||
} else {
|
||||
r.json()
|
||||
.then((json) => options.error(json))
|
||||
.catch((e) => options.error({ error: 'Failed to fetch.' }));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
options.error({ error: e.message });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
query(options) {
|
||||
this._makeRequest('/classes/' + options.collection, {
|
||||
method: 'POST',
|
||||
content: {
|
||||
_method: 'GET',
|
||||
where: options.where,
|
||||
limit: options.limit,
|
||||
skip: options.skip,
|
||||
include: Array.isArray(options.include) ? options.include.join(',') : options.include,
|
||||
keys: Array.isArray(options.select) ? options.select.join(',') : options.select,
|
||||
order: Array.isArray(options.sort) ? options.sort.join(',') : options.sort,
|
||||
count: options.count
|
||||
},
|
||||
success: function (response) {
|
||||
options.success(response.results, response.count);
|
||||
},
|
||||
error: function () {
|
||||
options.error();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
aggregate(options) {
|
||||
const args = [];
|
||||
|
||||
if (!options.group) {
|
||||
options.error('You need to provide group option.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.where) args.push('match=' + encodeURIComponent(JSON.stringify(options.where)));
|
||||
if (options.limit) args.push('limit=' + options.limit);
|
||||
if (options.skip) args.push('skip=' + options.skip);
|
||||
|
||||
const grouping = {
|
||||
objectId: null
|
||||
};
|
||||
|
||||
Object.keys(options.group).forEach((k) => {
|
||||
const _g = {};
|
||||
const group = options.group[k];
|
||||
if (group['avg'] !== undefined) _g['$avg'] = '$' + group['avg'];
|
||||
else if (group['sum'] !== undefined) _g['$sum'] = '$' + group['sum'];
|
||||
else if (group['max'] !== undefined) _g['$max'] = '$' + group['max'];
|
||||
else if (group['min'] !== undefined) _g['$min'] = '$' + group['min'];
|
||||
else if (group['distinct'] !== undefined) _g['$addToSet'] = '$' + group['distinct'];
|
||||
|
||||
grouping[k] = _g;
|
||||
});
|
||||
|
||||
args.push('group=' + JSON.stringify(grouping));
|
||||
|
||||
this._makeRequest('/aggregate/' + options.collection + (args.length > 0 ? '?' + args.join('&') : ''), {
|
||||
success: function (response) {
|
||||
const res = {};
|
||||
|
||||
if (!response.results || response.results.length !== 1) {
|
||||
options.success({}); // No result
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(options.group).forEach((k) => {
|
||||
res[k] = response.results[0][k];
|
||||
});
|
||||
|
||||
options.success(res);
|
||||
},
|
||||
error: function () {
|
||||
options.error();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
count(options) {
|
||||
const args = [];
|
||||
|
||||
if (options.where) args.push('where=' + encodeURIComponent(JSON.stringify(options.where)));
|
||||
args.push('limit=0');
|
||||
args.push('count=1');
|
||||
|
||||
this._makeRequest('/classes/' + options.collection + (args.length > 0 ? '?' + args.join('&') : ''), {
|
||||
success: function (response) {
|
||||
options.success(response.count);
|
||||
},
|
||||
error: function () {
|
||||
options.error();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
distinct(options) {
|
||||
const args = [];
|
||||
|
||||
if (options.where) args.push('where=' + encodeURIComponent(JSON.stringify(options.where)));
|
||||
args.push('distinct=' + options.property);
|
||||
|
||||
this._makeRequest('/aggregate/' + options.collection + (args.length > 0 ? '?' + args.join('&') : ''), {
|
||||
success: function (response) {
|
||||
options.success(response.results);
|
||||
},
|
||||
error: function () {
|
||||
options.error();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetch(options) {
|
||||
const args = [];
|
||||
|
||||
if (options.include)
|
||||
args.push('include=' + (Array.isArray(options.include) ? options.include.join(',') : options.include));
|
||||
|
||||
this._makeRequest(
|
||||
'/classes/' + options.collection + '/' + options.objectId + (args.length > 0 ? '?' + args.join('&') : ''),
|
||||
{
|
||||
method: 'GET',
|
||||
success: (response) => {
|
||||
options.success(response);
|
||||
this.events.emit('fetch', {
|
||||
type: 'fetch',
|
||||
objectId: options.objectId,
|
||||
object: response,
|
||||
collection: options.collection
|
||||
});
|
||||
},
|
||||
error: function (res) {
|
||||
options.error(res.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
create(options) {
|
||||
this._makeRequest('/classes/' + options.collection, {
|
||||
method: 'POST',
|
||||
content: Object.assign(
|
||||
_removeProtectedFields(_serializeObject(options.data, options.collection), options.collection),
|
||||
{ ACL: options.acl }
|
||||
),
|
||||
success: (response) => {
|
||||
const _obj = Object.assign({}, options.data, response);
|
||||
options.success(_obj);
|
||||
this.events.emit('create', {
|
||||
type: 'create',
|
||||
objectId: options.objectId,
|
||||
object: _obj,
|
||||
collection: options.collection
|
||||
});
|
||||
},
|
||||
error: function (res) {
|
||||
options.error(res.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
increment(options) {
|
||||
const data = {};
|
||||
|
||||
for (let key in options.properties) {
|
||||
data[key] = { __op: 'Increment', amount: options.properties[key] };
|
||||
}
|
||||
|
||||
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
||||
method: 'PUT',
|
||||
content: data,
|
||||
success: (response) => {
|
||||
options.success(response);
|
||||
},
|
||||
error: function (res) {
|
||||
options.error(res.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
save(options) {
|
||||
const _data = Object.assign({}, options.data);
|
||||
delete _data.createdAt;
|
||||
delete _data.updatedAt;
|
||||
|
||||
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
||||
method: 'PUT',
|
||||
content: Object.assign(_removeProtectedFields(_serializeObject(_data, options.collection), options.collection), {
|
||||
ACL: options.acl
|
||||
}),
|
||||
success: (response) => {
|
||||
options.success(response);
|
||||
this.events.emit('save', {
|
||||
type: 'save',
|
||||
objectId: options.objectId,
|
||||
object: Object.assign({}, options.data, response),
|
||||
collection: options.collection
|
||||
});
|
||||
},
|
||||
error: function (res) {
|
||||
options.error(res.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete(options) {
|
||||
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
||||
method: 'DELETE',
|
||||
success: () => {
|
||||
options.success();
|
||||
this.events.emit('delete', {
|
||||
type: 'delete',
|
||||
objectId: options.objectId,
|
||||
collection: options.collection
|
||||
});
|
||||
},
|
||||
error: function (res) {
|
||||
options.error(res.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addRelation(options) {
|
||||
const _content = {};
|
||||
_content[options.key] = {
|
||||
__op: 'AddRelation',
|
||||
objects: [
|
||||
{
|
||||
__type: 'Pointer',
|
||||
objectId: options.targetObjectId,
|
||||
className: options.targetClass
|
||||
}
|
||||
]
|
||||
};
|
||||
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
||||
method: 'PUT',
|
||||
content: _content,
|
||||
success: function (response) {
|
||||
options.success(response);
|
||||
},
|
||||
error: function (res) {
|
||||
options.error(res.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeRelation(options) {
|
||||
const _content = {};
|
||||
_content[options.key] = {
|
||||
__op: 'RemoveRelation',
|
||||
objects: [
|
||||
{
|
||||
__type: 'Pointer',
|
||||
objectId: options.targetObjectId,
|
||||
className: options.targetClass
|
||||
}
|
||||
]
|
||||
};
|
||||
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
||||
method: 'PUT',
|
||||
content: _content,
|
||||
success: function (response) {
|
||||
options.success(response);
|
||||
},
|
||||
error: function (res) {
|
||||
options.error(res.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile(options) {
|
||||
this._makeRequest('/files/' + options.file.name, {
|
||||
method: 'POST',
|
||||
content: options.file,
|
||||
contentType: options.file.type,
|
||||
success: (response) => options.success(Object.assign({}, options.data, response)),
|
||||
error: (err) => options.error(err),
|
||||
onUploadProgress: options.onUploadProgress
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Users holding the master key are allowed to delete files
|
||||
*
|
||||
* @param {{
|
||||
* file: {
|
||||
* name: string;
|
||||
* }
|
||||
* }} options
|
||||
*/
|
||||
deleteFile(options) {
|
||||
this._makeRequest('/files/' + options.file.name, {
|
||||
method: 'DELETE',
|
||||
success: (response) => options.success(Object.assign({}, options.data, response)),
|
||||
error: (err) => options.error(err)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _isArrayOfObjects(a) {
|
||||
if (!Array.isArray(a)) return false;
|
||||
for (var i = 0; i < a.length; i++) if (typeof a[i] !== 'object' || a[i] === null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function _toJSON(obj) {
|
||||
if (obj instanceof Model) {
|
||||
var res = {};
|
||||
for (var key in obj.data) {
|
||||
res[key] = _toJSON(obj.data[key]);
|
||||
}
|
||||
return res;
|
||||
} else if (obj instanceof Collection) {
|
||||
var res = [];
|
||||
obj.items.forEach((m) => {
|
||||
res.push(_toJSON(m));
|
||||
});
|
||||
return res;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function _serializeObject(data, collectionName, modelScope) {
|
||||
if (CloudStore._collections[collectionName]) var schema = CloudStore._collections[collectionName].schema;
|
||||
|
||||
for (var key in data) {
|
||||
var _type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined;
|
||||
|
||||
if (data[key] === undefined || data[key] === null) {
|
||||
// Keep null and undefined as is
|
||||
} else if (_type === 'Pointer' && typeof data[key] === 'string') {
|
||||
// This is a string pointer to an object
|
||||
data[key] = {
|
||||
__type: 'Pointer',
|
||||
className: schema.properties[key].targetClass,
|
||||
objectId: data[key]
|
||||
};
|
||||
} else if (_type === 'Pointer' && typeof data[key] === 'object' && (modelScope || Model).instanceOf(data[key])) {
|
||||
// This is an embedded object that should be stored as pointer
|
||||
data[key] = {
|
||||
__type: 'Pointer',
|
||||
className: schema.properties[key].targetClass,
|
||||
objectId: data[key].getId()
|
||||
};
|
||||
} else if (_type === 'Date' && (typeof data[key] === 'string' || data[key] instanceof Date)) {
|
||||
data[key] = {
|
||||
__type: 'Date',
|
||||
iso: data[key] instanceof Date ? data[key].toISOString() : data[key]
|
||||
};
|
||||
} else if (_type === 'File' && data[key] instanceof CloudFile) {
|
||||
const cloudFile = data[key];
|
||||
data[key] = {
|
||||
__type: 'File',
|
||||
url: cloudFile.getUrl(),
|
||||
name: cloudFile.getName()
|
||||
};
|
||||
} else if (_type === 'Array' && typeof data[key] === 'string' && Collection.exists(data[key])) {
|
||||
data[key] = _toJSON(Collection.get(data[key]));
|
||||
} else if (_type === 'Object' && typeof data[key] === 'string' && (modelScope || Model).exists(data[key])) {
|
||||
data[key] = _toJSON((modelScope || Model).get(data[key]));
|
||||
} else if (_type === 'GeoPoint' && typeof data[key] === 'object') {
|
||||
data[key] = {
|
||||
__type: 'GeoPoint',
|
||||
latitude: Number(data[key].latitude),
|
||||
longitude: Number(data[key].longitude)
|
||||
};
|
||||
} else data[key] = _toJSON(data[key]);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function _deserializeJSON(data, type, modelScope) {
|
||||
if (data === undefined) return;
|
||||
if (data === null) return null;
|
||||
|
||||
if (type === 'Relation' && data.__type === 'Relation') {
|
||||
return undefined; // Ignore relation fields
|
||||
} else if (type === 'Pointer' && data.__type === 'Pointer') {
|
||||
// This is a pointer type, resolve into id
|
||||
return data.objectId;
|
||||
} else if (type === 'Date' && data.__type === 'Date') {
|
||||
return new Date(data.iso);
|
||||
} else if (type === 'Date' && typeof data === 'string') {
|
||||
return new Date(data);
|
||||
} else if (type === 'File' && data.__type === 'File') {
|
||||
return new CloudFile(data);
|
||||
} else if (type === 'GeoPoint' && data.__type === 'GeoPoint') {
|
||||
return {
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude
|
||||
};
|
||||
} else if (_isArrayOfObjects(data)) {
|
||||
var a = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
a.push(_deserializeJSON(data[i], undefined, modelScope));
|
||||
}
|
||||
var c = Collection.get();
|
||||
c.set(a);
|
||||
return c;
|
||||
} else if (Array.isArray(data)) return data;
|
||||
// This is an array with mixed data, just return it
|
||||
else if (data && data.__type === 'Object' && data.className !== undefined && data.objectId !== undefined) {
|
||||
const _data = Object.assign({}, data);
|
||||
delete _data.className;
|
||||
delete _data.__type;
|
||||
return _fromJSON(_data, data.className, modelScope);
|
||||
} else if (typeof data === 'object' && data !== null) {
|
||||
var m = (modelScope || Model).get();
|
||||
for (var key in data) {
|
||||
m.set(key, _deserializeJSON(data[key], undefined, modelScope));
|
||||
}
|
||||
return m;
|
||||
} else return data;
|
||||
}
|
||||
|
||||
function _fromJSON(item, collectionName, modelScope) {
|
||||
const m = (modelScope || Model).get(item.objectId);
|
||||
m._class = collectionName;
|
||||
|
||||
if (collectionName !== undefined && CloudStore._collections[collectionName] !== undefined)
|
||||
var schema = CloudStore._collections[collectionName].schema;
|
||||
|
||||
for (var key in item) {
|
||||
if (key === 'objectId' || key === 'ACL') continue;
|
||||
|
||||
var _type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined;
|
||||
|
||||
m.set(key, _deserializeJSON(item[key], _type, modelScope));
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
CloudStore._fromJSON = _fromJSON;
|
||||
CloudStore._deserializeJSON = _deserializeJSON;
|
||||
CloudStore._serializeObject = _serializeObject;
|
||||
|
||||
CloudStore.forScope = (modelScope) => {
|
||||
if (modelScope === undefined) return CloudStore.instance;
|
||||
if (modelScope._cloudStore) return modelScope._cloudStore;
|
||||
|
||||
modelScope._cloudStore = new CloudStore(modelScope);
|
||||
return modelScope._cloudStore;
|
||||
};
|
||||
|
||||
var _instance;
|
||||
Object.defineProperty(CloudStore, 'instance', {
|
||||
get: function () {
|
||||
if (_instance === undefined) _instance = new CloudStore();
|
||||
return _instance;
|
||||
}
|
||||
});
|
||||
|
||||
var _collections;
|
||||
Object.defineProperty(CloudStore, '_collections', {
|
||||
get: function () {
|
||||
if (_collections === undefined) {
|
||||
_collections = {};
|
||||
const dbCollections = NoodlRuntime.instance.getMetaData('dbCollections') || [];
|
||||
dbCollections.forEach((c) => {
|
||||
_collections[c.name] = c;
|
||||
});
|
||||
|
||||
const systemCollections = NoodlRuntime.instance.getMetaData('systemCollections') || [];
|
||||
systemCollections.forEach((c) => {
|
||||
_collections[c.name] = c;
|
||||
});
|
||||
}
|
||||
return _collections;
|
||||
}
|
||||
});
|
||||
|
||||
CloudStore.invalidateCollections = () => {
|
||||
_collections = undefined;
|
||||
};
|
||||
|
||||
module.exports = CloudStore;
|
||||
112
packages/noodl-runtime/src/api/configservice.js
Normal file
112
packages/noodl-runtime/src/api/configservice.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const NoodlRuntime = require('../../../noodl-runtime');
|
||||
|
||||
class ConfigService {
|
||||
constructor() {
|
||||
this.cacheDuration = 15 * 60 * 1000; // 15 min cache
|
||||
}
|
||||
|
||||
_makeRequest(path, options) {
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// Running in browser
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(xhr.response);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr.status === 200 || xhr.status === 201) {
|
||||
options.success(json);
|
||||
} else options.error(json || { error: xhr.responseText, status: xhr.status });
|
||||
}
|
||||
};
|
||||
|
||||
const cloudServices = NoodlRuntime.instance.getMetaData('cloudservices');
|
||||
const appId = cloudServices.appId;
|
||||
const endpoint = cloudServices.endpoint;
|
||||
xhr.open('GET', endpoint + path, true);
|
||||
|
||||
xhr.setRequestHeader('X-Parse-Application-Id', appId);
|
||||
|
||||
xhr.send();
|
||||
} else {
|
||||
// Running in cloud runtime
|
||||
const endpoint = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.endpoint : this.endpoint;
|
||||
const appId = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.appId : this.appId;
|
||||
const masterKey = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.masterKey : undefined;
|
||||
|
||||
fetch(endpoint + path, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': appId,
|
||||
'X-Parse-Master-Key': masterKey
|
||||
}
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.status === 200 || r.status === 201) {
|
||||
r.json()
|
||||
.then((json) => options.success(json))
|
||||
.catch((e) =>
|
||||
options.error({
|
||||
error: 'Config: Failed to get json result.'
|
||||
})
|
||||
);
|
||||
} else {
|
||||
r.json()
|
||||
.then((json) => options.error(json))
|
||||
.catch((e) => options.error({ error: 'Failed to fetch.' }));
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
options.error({ error: e.message });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._makeRequest('/config', {
|
||||
success: (config) => {
|
||||
resolve(config.params || {});
|
||||
},
|
||||
error: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
if (this.configCachePending) return this.configCachePending;
|
||||
|
||||
if (!this.configCache) {
|
||||
this.configCachePending = this._getConfig();
|
||||
|
||||
this.configCache = await this.configCachePending;
|
||||
delete this.configCachePending;
|
||||
this.ttl = Date.now() + this.cacheDuration;
|
||||
return this.configCache;
|
||||
} else {
|
||||
// Update cache if ttl has passed
|
||||
if (Date.now() > this.ttl) {
|
||||
this._getConfig().then((config) => {
|
||||
this.configCache = config;
|
||||
this.ttl = Date.now() + this.cacheDuration;
|
||||
});
|
||||
}
|
||||
|
||||
// But return currently cached
|
||||
return this.configCache;
|
||||
}
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
delete this.configCache;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigService.instance = new ConfigService();
|
||||
|
||||
module.exports = ConfigService;
|
||||
310
packages/noodl-runtime/src/api/queryutils.js
Normal file
310
packages/noodl-runtime/src/api/queryutils.js
Normal file
@@ -0,0 +1,310 @@
|
||||
const CloudStore = require('./cloudstore');
|
||||
const Model = require('../model');
|
||||
|
||||
function convertVisualFilter(query, options) {
|
||||
var inputs = options.queryParameters;
|
||||
|
||||
if (query.combinator !== undefined && query.rules !== undefined) {
|
||||
if (query.rules.length === 0) return;
|
||||
else if (query.rules.length === 1) return convertVisualFilter(query.rules[0], options);
|
||||
else {
|
||||
const _res = {};
|
||||
const _op = '$' + query.combinator;
|
||||
_res[_op] = [];
|
||||
query.rules.forEach((r) => {
|
||||
var cond = convertVisualFilter(r, options);
|
||||
if (cond !== undefined) _res[_op].push(cond);
|
||||
});
|
||||
|
||||
return _res;
|
||||
}
|
||||
} else if (query.operator === 'related to') {
|
||||
var value = query.input !== undefined ? inputs[query.input] : undefined;
|
||||
if (value === undefined) return;
|
||||
|
||||
return {
|
||||
$relatedTo: {
|
||||
object: {
|
||||
__type: 'Pointer',
|
||||
objectId: value,
|
||||
className: query.relatedTo
|
||||
},
|
||||
key: query.relationProperty
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const _res = {};
|
||||
var cond;
|
||||
var value = query.input !== undefined ? inputs[query.input] : query.value;
|
||||
|
||||
if (query.operator === 'exist') {
|
||||
_res[query.property] = { $exists: true };
|
||||
return _res;
|
||||
}
|
||||
else if (query.operator === 'not exist') {
|
||||
_res[query.property] = { $exists: false };
|
||||
return _res;
|
||||
}
|
||||
|
||||
if (value === undefined) return;
|
||||
|
||||
if (CloudStore._collections[options.collectionName])
|
||||
var schema = CloudStore._collections[options.collectionName].schema;
|
||||
|
||||
var propertyType =
|
||||
schema && schema.properties && schema.properties[query.property]
|
||||
? schema.properties[query.property].type
|
||||
: undefined;
|
||||
|
||||
if (propertyType === 'Date') {
|
||||
if (!(value instanceof Date)) value = new Date(value.toString());
|
||||
value = { __type: 'Date', iso: value.toISOString() };
|
||||
}
|
||||
|
||||
if (query.operator === 'greater than') cond = { $gt: value };
|
||||
else if (query.operator === 'greater than or equal to') cond = { $gte: value };
|
||||
else if (query.operator === 'less than') cond = { $lt: value };
|
||||
else if (query.operator === 'less than or equal to') cond = { $lte: value };
|
||||
else if (query.operator === 'equal to') cond = { $eq: value };
|
||||
else if (query.operator === 'not equal to') cond = { $ne: value };
|
||||
else if (query.operator === 'points to') {
|
||||
var targetClass =
|
||||
schema && schema.properties && schema.properties[query.property]
|
||||
? schema.properties[query.property].targetClass
|
||||
: undefined;
|
||||
|
||||
cond = {
|
||||
$eq: { __type: 'Pointer', objectId: value, className: targetClass }
|
||||
};
|
||||
} else if (query.operator === 'contain') {
|
||||
cond = { $regex: value, $options: 'i' };
|
||||
}
|
||||
|
||||
|
||||
_res[query.property] = cond;
|
||||
|
||||
return _res;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesQuery(model, query) {
|
||||
var match = true;
|
||||
|
||||
if (query === undefined) return true;
|
||||
|
||||
if (query['$and'] !== undefined) {
|
||||
query['$and'].forEach((q) => {
|
||||
match &= matchesQuery(model, q);
|
||||
});
|
||||
} else if (query['$or'] !== undefined) {
|
||||
match = false;
|
||||
query['$or'].forEach((q) => {
|
||||
match |= matchesQuery(model, q);
|
||||
});
|
||||
} else {
|
||||
var keys = Object.keys(query);
|
||||
keys.forEach((k) => {
|
||||
if (k === 'objectId') {
|
||||
if (query[k]['$eq'] !== undefined) match &= model.getId() === query[k]['$eq'];
|
||||
else if (query[k]['$in'] !== undefined) match &= query[k]['$in'].indexOf(model.getId()) !== -1;
|
||||
} else if (k === '$relatedTo') {
|
||||
match = false; // cannot resolve relation queries locally
|
||||
} else {
|
||||
var value = model.get(k);
|
||||
if (query[k]['$eq'] !== undefined && query[k]['$eq'].__type === 'Pointer')
|
||||
match &= value === query[k]['$eq'].objectId;
|
||||
else if (query[k]['$eq'] !== undefined) match &= value == query[k]['$eq'];
|
||||
else if (query[k]['$ne'] !== undefined) match &= value != query[k]['$ne'];
|
||||
else if (query[k]['$lt'] !== undefined) match &= value < query[k]['$lt'];
|
||||
else if (query[k]['$lte'] !== undefined) match &= value <= query[k]['$lt'];
|
||||
else if (query[k]['$gt'] !== undefined) match &= value > query[k]['$gt'];
|
||||
else if (query[k]['$gte'] !== undefined) match &= value >= query[k]['$gte'];
|
||||
else if (query[k]['$exists'] !== undefined) match &= value !== undefined;
|
||||
else if (query[k]['$in'] !== undefined) match &= query[k]['$in'].indexOf(value) !== -1;
|
||||
else if (query[k]['$nin'] !== undefined) match &= query[k]['$in'].indexOf(value) === -1;
|
||||
else if (query[k]['$regex'] !== undefined)
|
||||
match &= new RegExp(query[k]['$regex'], query[k]['$options']).test(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
function compareObjects(sort, a, b) {
|
||||
for (var i = 0; i < sort.length; i++) {
|
||||
let _s = sort[i];
|
||||
if (_s[0] === '-') {
|
||||
// Descending
|
||||
let prop = _s.substring(1);
|
||||
if (a.get(prop) > b.get(prop)) return -1;
|
||||
else if (a.get(prop) < b.get(prop)) return 1;
|
||||
} else {
|
||||
// Ascending
|
||||
if (a.get(_s) > b.get(_s)) return 1;
|
||||
else if (a.get(_s) < b.get(_s)) return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function convertVisualSorting(sorting) {
|
||||
return sorting.map((s) => {
|
||||
return (s.order === 'descending' ? '-' : '') + s.property;
|
||||
});
|
||||
}
|
||||
|
||||
function _value(v) {
|
||||
if (v instanceof Date && typeof v.toISOString === 'function') {
|
||||
return {
|
||||
__type: 'Date',
|
||||
iso: v.toISOString()
|
||||
};
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function convertFilterOp(filter, options) {
|
||||
const keys = Object.keys(filter);
|
||||
if (keys.length === 0) return {};
|
||||
if (keys.length !== 1) return options.error('Filter must only have one key found ' + keys.join(','));
|
||||
|
||||
const res = {};
|
||||
const key = keys[0];
|
||||
if (filter['and'] !== undefined && Array.isArray(filter['and'])) {
|
||||
res['$and'] = filter['and'].map((f) => convertFilterOp(f, options));
|
||||
} else if (filter['or'] !== undefined && Array.isArray(filter['or'])) {
|
||||
res['$or'] = filter['or'].map((f) => convertFilterOp(f, options));
|
||||
} else if (filter['idEqualTo'] !== undefined) {
|
||||
res['objectId'] = { $eq: filter['idEqualTo'] };
|
||||
} else if (filter['idContainedIn'] !== undefined) {
|
||||
res['objectId'] = { $in: filter['idContainedIn'] };
|
||||
} else if (filter['relatedTo'] !== undefined) {
|
||||
var modelId = filter['relatedTo']['id'];
|
||||
if (modelId === undefined) return options.error('Must provide id in relatedTo filter');
|
||||
|
||||
var relationKey = filter['relatedTo']['key'];
|
||||
if (relationKey === undefined) return options.error('Must provide key in relatedTo filter');
|
||||
|
||||
var m = (options.modelScope || Model).get(modelId);
|
||||
res['$relatedTo'] = {
|
||||
object: {
|
||||
__type: 'Pointer',
|
||||
objectId: modelId,
|
||||
className: m._class
|
||||
},
|
||||
key: relationKey
|
||||
};
|
||||
} else if (typeof filter[key] === 'object') {
|
||||
const opAndValue = filter[key];
|
||||
if (opAndValue['equalTo'] !== undefined) res[key] = { $eq: _value(opAndValue['equalTo']) };
|
||||
else if (opAndValue['notEqualTo'] !== undefined) res[key] = { $ne: _value(opAndValue['notEqualTo']) };
|
||||
else if (opAndValue['lessThan'] !== undefined) res[key] = { $lt: _value(opAndValue['lessThan']) };
|
||||
else if (opAndValue['greaterThan'] !== undefined) res[key] = { $gt: _value(opAndValue['greaterThan']) };
|
||||
else if (opAndValue['lessThanOrEqualTo'] !== undefined)
|
||||
res[key] = { $lte: _value(opAndValue['lessThanOrEqualTo']) };
|
||||
else if (opAndValue['greaterThanOrEqualTo'] !== undefined)
|
||||
res[key] = { $gte: _value(opAndValue['greaterThanOrEqualTo']) };
|
||||
else if (opAndValue['exists'] !== undefined) res[key] = { $exists: opAndValue['exists'] };
|
||||
else if (opAndValue['containedIn'] !== undefined) res[key] = { $in: opAndValue['containedIn'] };
|
||||
else if (opAndValue['notContainedIn'] !== undefined) res[key] = { $nin: opAndValue['notContainedIn'] };
|
||||
else if (opAndValue['pointsTo'] !== undefined) {
|
||||
var m = (options.modelScope || Model).get(opAndValue['pointsTo']);
|
||||
if (CloudStore._collections[options.collectionName])
|
||||
var schema = CloudStore._collections[options.collectionName].schema;
|
||||
|
||||
var targetClass =
|
||||
schema && schema.properties && schema.properties[key] ? schema.properties[key].targetClass : undefined;
|
||||
var type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined;
|
||||
|
||||
if (type === 'Relation') {
|
||||
res[key] = {
|
||||
__type: 'Pointer',
|
||||
objectId: opAndValue['pointsTo'],
|
||||
className: targetClass
|
||||
};
|
||||
} else {
|
||||
if (Array.isArray(opAndValue['pointsTo']))
|
||||
res[key] = {
|
||||
$in: opAndValue['pointsTo'].map((v) => {
|
||||
return { __type: 'Pointer', objectId: v, className: targetClass };
|
||||
})
|
||||
};
|
||||
else
|
||||
res[key] = {
|
||||
$eq: {
|
||||
__type: 'Pointer',
|
||||
objectId: opAndValue['pointsTo'],
|
||||
className: targetClass
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (opAndValue['matchesRegex'] !== undefined) {
|
||||
res[key] = {
|
||||
$regex: opAndValue['matchesRegex'],
|
||||
$options: opAndValue['options']
|
||||
};
|
||||
} else if (opAndValue['text'] !== undefined && opAndValue['text']['search'] !== undefined) {
|
||||
var _v = opAndValue['text']['search'];
|
||||
if (typeof _v === 'string') res[key] = { $text: { $search: { $term: _v, $caseSensitive: false } } };
|
||||
else
|
||||
res[key] = {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: _v.term,
|
||||
$language: _v.language,
|
||||
$caseSensitive: _v.caseSensitive,
|
||||
$diacriticSensitive: _v.diacriticSensitive
|
||||
}
|
||||
}
|
||||
};
|
||||
// Geo points
|
||||
} else if (opAndValue['nearSphere'] !== undefined) {
|
||||
var _v = opAndValue['nearSphere'];
|
||||
res[key] = {
|
||||
$nearSphere: {
|
||||
__type: "GeoPoint",
|
||||
latitude: _v.latitude,
|
||||
longitude: _v.longitude,
|
||||
},
|
||||
$maxDistanceInMiles:_v.$maxDistanceInMiles,
|
||||
$maxDistanceInKilometers:_v.maxDistanceInKilometers,
|
||||
$maxDistanceInRadians:_v.maxDistanceInRadians
|
||||
};
|
||||
} else if (opAndValue['withinBox'] !== undefined) {
|
||||
var _v = opAndValue['withinBox'];
|
||||
res[key] = {
|
||||
$within:{
|
||||
$box: _v.map(gp => ({
|
||||
__type:"GeoPoint",
|
||||
latitude:gp.latitude,
|
||||
longitude:gp.longitude
|
||||
}))
|
||||
}
|
||||
};
|
||||
} else if (opAndValue['withinPolygon'] !== undefined) {
|
||||
var _v = opAndValue['withinPolygon'];
|
||||
res[key] = {
|
||||
$geoWithin:{
|
||||
$polygon: _v.map(gp => ({
|
||||
__type:"GeoPoint",
|
||||
latitude:gp.latitude,
|
||||
longitude:gp.longitude
|
||||
}))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} else {
|
||||
options.error('Unrecognized filter keys ' + keys.join(','));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
convertVisualFilter,
|
||||
compareObjects,
|
||||
matchesQuery,
|
||||
convertVisualSorting,
|
||||
convertFilterOp
|
||||
};
|
||||
276
packages/noodl-runtime/src/api/records.js
Normal file
276
packages/noodl-runtime/src/api/records.js
Normal file
@@ -0,0 +1,276 @@
|
||||
const CloudStore = require('./cloudstore');
|
||||
const QueryUtils = require('./queryutils');
|
||||
const Model = require('../model');
|
||||
|
||||
function createRecordsAPI(modelScope) {
|
||||
let _cloudstore;
|
||||
const cloudstore = () => {
|
||||
// We must create the cloud store just in time so all meta data is loaded
|
||||
if (!_cloudstore) _cloudstore = new CloudStore(modelScope);
|
||||
return _cloudstore;
|
||||
};
|
||||
|
||||
return {
|
||||
async query(className, query, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
cloudstore().query({
|
||||
collection: className,
|
||||
where: QueryUtils.convertFilterOp(query || {}, {
|
||||
collectionName: className,
|
||||
error: (e) => reject(e),
|
||||
modelScope
|
||||
}),
|
||||
limit: options ? options.limit : undefined,
|
||||
sort: options ? options.sort : undefined,
|
||||
skip: options ? options.skip : undefined,
|
||||
include: options ? options.include : undefined,
|
||||
select: options ? options.select : undefined,
|
||||
count: options ? options.count : undefined,
|
||||
success: (results,count) => {
|
||||
const _results = results.map((r) => cloudstore()._fromJSON(r, className));
|
||||
if(count !== undefined) resolve({results:_results,count});
|
||||
else resolve(_results);
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to query.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async count(className, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
cloudstore().count({
|
||||
collection: className,
|
||||
where: query
|
||||
? QueryUtils.convertFilterOp(query || {}, {
|
||||
collectionName: className,
|
||||
error: (e) => reject(e),
|
||||
modelScope
|
||||
})
|
||||
: undefined,
|
||||
success: (count) => {
|
||||
resolve(count);
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to query.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async distinct(className, property, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
cloudstore().distinct({
|
||||
collection: className,
|
||||
property,
|
||||
where: query
|
||||
? QueryUtils.convertFilterOp(query || {}, {
|
||||
collectionName: className,
|
||||
error: (e) => reject(e),
|
||||
modelScope
|
||||
})
|
||||
: undefined,
|
||||
success: (results) => {
|
||||
resolve(results);
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to query.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async aggregate(className, group, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
cloudstore().aggregate({
|
||||
collection: className,
|
||||
group,
|
||||
where: query
|
||||
? QueryUtils.convertFilterOp(query || {}, {
|
||||
collectionName: className,
|
||||
error: (e) => reject(e),
|
||||
modelScope
|
||||
})
|
||||
: undefined,
|
||||
success: (results) => {
|
||||
resolve(results);
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to aggregate.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async fetch(objectOrId, options) {
|
||||
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
|
||||
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!className) return reject('No class name specified');
|
||||
|
||||
cloudstore().fetch({
|
||||
collection: className,
|
||||
objectId: objectOrId,
|
||||
include: options ? options.include : undefined,
|
||||
success: function (response) {
|
||||
var record = cloudstore()._fromJSON(response, className);
|
||||
resolve(record);
|
||||
},
|
||||
error: function (err) {
|
||||
reject(Error(err || 'Failed to fetch.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async increment(objectOrId, properties, options) {
|
||||
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
|
||||
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!className) return reject('No class name specified');
|
||||
|
||||
cloudstore().increment({
|
||||
collection: className,
|
||||
objectId: objectOrId,
|
||||
properties,
|
||||
success: (response) => {
|
||||
cloudstore()._fromJSON(Object.assign({ objectId: objectOrId }, response), className); // Update values
|
||||
|
||||
resolve();
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to increment.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async save(objectOrId, properties, options) {
|
||||
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
|
||||
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
|
||||
|
||||
const model = (modelScope || Model).get(objectOrId);
|
||||
if (properties) {
|
||||
Object.keys(properties).forEach((p) => {
|
||||
model.set(p, properties[p]);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!className) return reject('No class name specified');
|
||||
|
||||
cloudstore().save({
|
||||
collection: className,
|
||||
objectId: objectOrId,
|
||||
data: properties || model.data,
|
||||
acl: options ? options.acl : undefined,
|
||||
success: (response) => {
|
||||
cloudstore()._fromJSON(Object.assign({ objectId: objectOrId }, response), className); // Assign updated at
|
||||
resolve();
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to save.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async create(className, properties, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
cloudstore().create({
|
||||
collection: className,
|
||||
data: properties,
|
||||
acl: options ? options.acl : undefined,
|
||||
success: (data) => {
|
||||
// Successfully created
|
||||
const m = cloudstore()._fromJSON(data, className);
|
||||
resolve(m);
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to insert.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async delete(objectOrId, options) {
|
||||
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
|
||||
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!className) return reject('No class name specified');
|
||||
|
||||
cloudstore().delete({
|
||||
collection: className,
|
||||
objectId: objectOrId,
|
||||
success: () => {
|
||||
(modelScope || Model).get(objectOrId).notify('delete');
|
||||
resolve();
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to delete.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async addRelation(options) {
|
||||
const recordId = options.recordId || options.record.getId();
|
||||
const className = options.className || (modelScope || Model).get(recordId)._class;
|
||||
|
||||
const targetRecordId = options.targetRecordId || options.targetRecord.getId();
|
||||
const targetClassName = options.targetClassName || (modelScope || Model).get(targetRecordId)._class;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!className) return reject('No class name specified');
|
||||
if (!targetClassName) return reject('No target class name specified');
|
||||
|
||||
cloudstore().addRelation({
|
||||
collection: className,
|
||||
objectId: recordId,
|
||||
key: options.key,
|
||||
targetObjectId: targetRecordId,
|
||||
targetClass: targetClassName,
|
||||
success: (response) => {
|
||||
resolve();
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(err || 'Failed to add relation.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async removeRelation(options) {
|
||||
const recordId = options.recordId || options.record.getId();
|
||||
const className = options.className || (modelScope || Model).get(recordId)._class;
|
||||
|
||||
const targetRecordId = options.targetRecordId || options.targetRecord.getId();
|
||||
const targetClassName = options.targetClassName || (modelScope || Model).get(targetRecordId)._class;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!className) return reject('No class name specified');
|
||||
if (!targetClassName) return reject('No target class name specified');
|
||||
|
||||
cloudstore().removeRelation({
|
||||
collection: className,
|
||||
objectId: recordId,
|
||||
key: options.key,
|
||||
targetObjectId: targetRecordId,
|
||||
targetClass: targetClassName,
|
||||
success: (response) => {
|
||||
resolve();
|
||||
},
|
||||
error: (err) => {
|
||||
reject(Error(rr || 'Failed to add relation.'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createRecordsAPI;
|
||||
20
packages/noodl-runtime/src/async-pool.js
Normal file
20
packages/noodl-runtime/src/async-pool.js
Normal file
@@ -0,0 +1,20 @@
|
||||
async function asyncPool(poolLimit, values, iteratorFn) {
|
||||
const ret = [];
|
||||
const executing = [];
|
||||
for (const item of values) {
|
||||
const p = Promise.resolve().then(() => iteratorFn(item, values));
|
||||
ret.push(p);
|
||||
|
||||
if (poolLimit <= values.length) {
|
||||
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
|
||||
executing.push(e);
|
||||
if (executing.length >= poolLimit) {
|
||||
await Promise.race(executing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(ret);
|
||||
}
|
||||
|
||||
module.exports = asyncPool;
|
||||
464
packages/noodl-runtime/src/collection.js
Normal file
464
packages/noodl-runtime/src/collection.js
Normal file
@@ -0,0 +1,464 @@
|
||||
"use strict";
|
||||
|
||||
var Model = require("./model");
|
||||
|
||||
// Get and set proxy
|
||||
/*const proxies = {}
|
||||
const _collectionProxyHandler = {
|
||||
get: function(target,prop,receiver) {
|
||||
if(typeof target[prop] === 'function')
|
||||
return target[prop].bind(target);
|
||||
else if(prop === 'length')
|
||||
return target.size()
|
||||
else if(target.items[prop] !== undefined)
|
||||
return target.get(prop)
|
||||
else
|
||||
return Reflect.get(target,prop,receiver)
|
||||
},
|
||||
set: function(obj,prop,value) {
|
||||
if(prop === 'id') {
|
||||
console.log(`Noodl.Object warning: id is readonly (Id is ${obj.id}, trying to set to ${value})`);
|
||||
return true; //if a proxy doesn't return true an exception will be thrown
|
||||
}
|
||||
else
|
||||
return Reflect.set(target,prop,receiver)
|
||||
}
|
||||
}
|
||||
|
||||
function Collection(id) {
|
||||
this.id = id;
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
var collections = Collection._collections = {};
|
||||
|
||||
Collection.create = function(items) {
|
||||
const name = Model.guid();
|
||||
collections[name] = new Collection(name);
|
||||
if(items) {
|
||||
collections[name].set(items);
|
||||
}
|
||||
return collections[name];
|
||||
}
|
||||
|
||||
Collection.get = function(name) {
|
||||
if(name === undefined) name = Model.guid();
|
||||
if(!collections[name]) {
|
||||
collections[name] = new Collection(name);
|
||||
proxies[name] = new Proxy(collections[name],_collectionProxyHandler);
|
||||
}
|
||||
return proxies[name];
|
||||
}
|
||||
|
||||
Collection.instanceOf = function(collection) {
|
||||
return collection && (collection instanceof Collection || collection.target instanceof Collection);
|
||||
}
|
||||
|
||||
Collection.exists = function(name) {
|
||||
return collections[name] !== undefined;
|
||||
}
|
||||
|
||||
Collection.prototype.getId = function() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
Collection.prototype.on = function(event,listener) {
|
||||
if(!this.listeners) this.listeners = {};
|
||||
if(!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(listener);
|
||||
}
|
||||
|
||||
Collection.prototype.off = function(event,listener) {
|
||||
if(!this.listeners) return;
|
||||
if(!this.listeners[event]) return;
|
||||
var idx = this.listeners[event].indexOf(listener);
|
||||
if(idx!==-1) this.listeners[event].splice(idx,1);
|
||||
}
|
||||
|
||||
Collection.prototype.notify = async function(event,args) {
|
||||
if(!this.listeners) return;
|
||||
if(!this.listeners[event]) return;
|
||||
|
||||
var l = this.listeners[event].slice(); //clone in case listeners array is modified in the callbacks
|
||||
for(var i = 0; i < l.length; i++) {
|
||||
await l[i](args);
|
||||
}
|
||||
}
|
||||
|
||||
Collection.prototype.set = function(src) {
|
||||
var length,i;
|
||||
|
||||
if(src === this) return;
|
||||
|
||||
function keyIndex(a) {
|
||||
var keys = {};
|
||||
var length = a.length;
|
||||
for(var i = 0; i < length; i++) {
|
||||
var item = a[i];
|
||||
keys[item.getId()] = item;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// Src can be a collection, or an array
|
||||
var src = Collection.instanceOf(src)?src.items:src;
|
||||
var bItems = [];
|
||||
length = src.length;
|
||||
for(i = 0; i < length; i++) {
|
||||
var item = src[i];
|
||||
if(Model.instanceOf(item))
|
||||
bItems.push(item);
|
||||
else
|
||||
bItems.push(Model.create(item));
|
||||
}
|
||||
|
||||
var aItems = this.items;
|
||||
var aKeys = keyIndex(aItems);
|
||||
var bKeys = keyIndex(bItems);
|
||||
|
||||
// First remove all items not in the new collection
|
||||
length = aItems.length;
|
||||
for(i = 0; i < length; i++) {
|
||||
if(!bKeys.hasOwnProperty(aItems[i].getId())) {
|
||||
// This item is not present in new collection, remove it
|
||||
this.removeAtIndex(i);
|
||||
i--;
|
||||
length--;
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder items
|
||||
for(i = 0; i < Math.min(aItems.length,bItems.length); i++) {
|
||||
if(aItems[i] !== bItems[i]) {
|
||||
if(aKeys.hasOwnProperty(bItems[i].getId())) {
|
||||
// The bItem exist in the collection but is in the wrong place
|
||||
this.remove(bItems[i]);
|
||||
}
|
||||
|
||||
// This is a new item, add it at correct index
|
||||
this.addAtIndex(bItems[i],i);
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining items
|
||||
for(i = aItems.length; i < bItems.length; i++) {
|
||||
this.add(bItems[i]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Collection.prototype.contains = function(item) {
|
||||
return this.items.indexOf(item)!==-1;
|
||||
}
|
||||
|
||||
Collection.prototype.add = async function(item) {
|
||||
if(this.contains(item)) return; // Already contains item
|
||||
|
||||
this.items.push(item);
|
||||
await this.notify('add',{item:item,index:this.items.length-1});
|
||||
await this.notify('change');
|
||||
await item.notify('add',{collection:this});
|
||||
}
|
||||
|
||||
Collection.prototype.addAtIndex = async function(item,index) {
|
||||
if(this.contains(item)) return; // Already contains item
|
||||
|
||||
this.items.splice(index,0,item);
|
||||
await this.notify('add',{item:item,index:index});
|
||||
await this.notify('change');
|
||||
await item.notify('add',{collection:this,index:index});
|
||||
}
|
||||
|
||||
Collection.prototype.removeAtIndex = async function(idx) {
|
||||
var item = this.items[idx];
|
||||
this.items.splice(idx,1);
|
||||
await this.notify('remove',{item:item,index:idx});
|
||||
await this.notify('change');
|
||||
await item.notify('remove',{collection:this});
|
||||
}
|
||||
|
||||
Collection.prototype.remove = function(item) {
|
||||
var idx = this.items.indexOf(item);
|
||||
if(idx !== -1) this.removeAtIndex(idx);
|
||||
}
|
||||
|
||||
Collection.prototype.size = function() {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
Collection.prototype.get = function(index) {
|
||||
return this.items[index];
|
||||
}
|
||||
|
||||
Collection.prototype.each = function(callback) {
|
||||
for(var i = 0; i < this.items.length; i++) {
|
||||
callback(this.items[i],i);
|
||||
}
|
||||
}
|
||||
|
||||
Collection.prototype.forEach = Collection.prototype.each;
|
||||
|
||||
Collection.prototype.map = function(fn) {
|
||||
return this.items.map(fn);
|
||||
}
|
||||
|
||||
Collection.prototype.filter = function(fn) {
|
||||
return this.items.filter(fn);
|
||||
}
|
||||
|
||||
Collection.prototype.find = function(predicate, thisArg) {
|
||||
return this.items.find(predicate, thisArg);
|
||||
}
|
||||
|
||||
Collection.prototype.findIndex = function(predicate, thisArg) {
|
||||
return this.items.findIndex(predicate, thisArg);
|
||||
}
|
||||
|
||||
Collection.prototype.toJSON = function() {
|
||||
return this.map(function(m) {
|
||||
return m.toJSON()
|
||||
})
|
||||
}*/
|
||||
|
||||
// ----
|
||||
Object.defineProperty(Array.prototype, "items", {
|
||||
enumerable: false,
|
||||
get() {
|
||||
return this;
|
||||
},
|
||||
set(data) {
|
||||
this.set(data);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Array.prototype, "each", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: Array.prototype.forEach,
|
||||
});
|
||||
Object.defineProperty(Array.prototype, "size", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function () {
|
||||
return this.length;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Array.prototype, "get", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function (index) {
|
||||
return this[index];
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Array.prototype, "getId", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function () {
|
||||
return this._id;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Array.prototype, "id", {
|
||||
enumerable: false,
|
||||
get() {
|
||||
return this.getId();
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Array.prototype, "set", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function (src) {
|
||||
var length, i;
|
||||
|
||||
if (src === this) return;
|
||||
|
||||
src = src || []; //handle if src is undefined
|
||||
|
||||
function keyIndex(a) {
|
||||
var keys = {};
|
||||
var length = a.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
var item = a[i];
|
||||
keys[item.getId()] = item;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// Src can be a collection, or an array
|
||||
var bItems = [];
|
||||
length = src.length;
|
||||
for (i = 0; i < length; i++) {
|
||||
var item = src[i];
|
||||
if (Model.instanceOf(item)) bItems.push(item);
|
||||
else bItems.push(Model.create(item));
|
||||
}
|
||||
|
||||
var aItems = this.items;
|
||||
var aKeys = keyIndex(aItems);
|
||||
var bKeys = keyIndex(bItems);
|
||||
|
||||
// First remove all items not in the new collection
|
||||
length = aItems.length;
|
||||
for (i = 0; i < length; i++) {
|
||||
if (!bKeys.hasOwnProperty(aItems[i].getId())) {
|
||||
// This item is not present in new collection, remove it
|
||||
this.removeAtIndex(i);
|
||||
i--;
|
||||
length--;
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder items
|
||||
for (i = 0; i < Math.min(aItems.length, bItems.length); i++) {
|
||||
if (aItems[i] !== bItems[i]) {
|
||||
if (aKeys.hasOwnProperty(bItems[i].getId())) {
|
||||
// The bItem exist in the collection but is in the wrong place
|
||||
this.remove(bItems[i]);
|
||||
}
|
||||
|
||||
// This is a new item, add it at correct index
|
||||
this.addAtIndex(bItems[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining items
|
||||
for (i = aItems.length; i < bItems.length; i++) {
|
||||
this.add(bItems[i]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "notify", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: async function (event, args) {
|
||||
if (!this._listeners) return;
|
||||
if (!this._listeners[event]) return;
|
||||
|
||||
var l = this._listeners[event].slice(); //clone in case listeners array is modified in the callbacks
|
||||
for (var i = 0; i < l.length; i++) {
|
||||
await l[i](args);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "contains", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function (item) {
|
||||
return this.indexOf(item) !== -1;
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "add", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: async function (item) {
|
||||
if (this.contains(item)) return; // Already contains item
|
||||
|
||||
this.items.push(item);
|
||||
await this.notify("add", { item: item, index: this.items.length - 1 });
|
||||
await this.notify("change");
|
||||
await item.notify("add", { collection: this });
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "remove", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function (item) {
|
||||
var idx = this.items.indexOf(item);
|
||||
if (idx !== -1) this.removeAtIndex(idx);
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "addAtIndex", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: async function (item, index) {
|
||||
if (this.contains(item)) return; // Already contains item
|
||||
|
||||
this.items.splice(index, 0, item);
|
||||
await this.notify("add", { item: item, index: index });
|
||||
await this.notify("change");
|
||||
await item.notify("add", { collection: this, index: index });
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "removeAtIndex", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: async function (idx) {
|
||||
var item = this.items[idx];
|
||||
this.items.splice(idx, 1);
|
||||
await this.notify("remove", { item: item, index: idx });
|
||||
await this.notify("change");
|
||||
await item.notify("remove", { collection: this });
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "on", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function (event, listener) {
|
||||
if (!this._listeners)
|
||||
Object.defineProperty(this, "_listeners", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: {},
|
||||
});
|
||||
if (!this._listeners[event]) this._listeners[event] = [];
|
||||
this._listeners[event].push(listener);
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, "off", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: function (event, listener) {
|
||||
if (!this._listeners) return;
|
||||
if (!this._listeners[event]) return;
|
||||
var idx = this._listeners[event].indexOf(listener);
|
||||
if (idx !== -1) this._listeners[event].splice(idx, 1);
|
||||
},
|
||||
});
|
||||
|
||||
class Collection extends Array {}
|
||||
|
||||
var collections = (Collection._collections = {});
|
||||
|
||||
Collection.create = function (items) {
|
||||
const name = Model.guid();
|
||||
collections[name] = new Collection();
|
||||
Object.defineProperty(collections[name], "_id", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: name,
|
||||
});
|
||||
if (items) {
|
||||
collections[name].set(items);
|
||||
}
|
||||
return collections[name];
|
||||
};
|
||||
|
||||
Collection.get = function (name) {
|
||||
if (name === undefined) name = Model.guid();
|
||||
if (!collections[name]) {
|
||||
collections[name] = new Collection();
|
||||
Object.defineProperty(collections[name], "_id", {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
return collections[name];
|
||||
};
|
||||
|
||||
Collection.instanceOf = function (collection) {
|
||||
return collection instanceof Collection;
|
||||
};
|
||||
|
||||
Collection.exists = function (name) {
|
||||
return collections[name] !== undefined;
|
||||
};
|
||||
|
||||
module.exports = Collection;
|
||||
19
packages/noodl-runtime/src/edgetriggeredinput.js
Normal file
19
packages/noodl-runtime/src/edgetriggeredinput.js
Normal file
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
function createSetter(args) {
|
||||
|
||||
var currentValue = false;
|
||||
|
||||
return function(value) {
|
||||
value = value ? true : false;
|
||||
//value changed from false to true
|
||||
if(value && currentValue === false) {
|
||||
args.valueChangedToTrue.call(this);
|
||||
}
|
||||
currentValue = value;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSetter: createSetter
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
//used to optimize warnings so we're not sending unneccessary warnings.
|
||||
//Improves editor performance, especially in larger projects
|
||||
|
||||
class ActiveWarnings {
|
||||
constructor() {
|
||||
this.currentWarnings = new Map();
|
||||
}
|
||||
|
||||
setWarning(nodeId, key, warning) {
|
||||
//Check if we've already sent this warning
|
||||
if (this.currentWarnings.has(nodeId)) {
|
||||
//we have sent warnings to this node before, check if we've sent this particular one before
|
||||
const warningKeys = this.currentWarnings.get(nodeId);
|
||||
if (warningKeys[key] === warning) {
|
||||
//we've already sent this warning, no need to send it again
|
||||
return false;
|
||||
}
|
||||
|
||||
//new warning, remember that we sent it
|
||||
warningKeys[key] = warning;
|
||||
return true;
|
||||
} else {
|
||||
//new warning, we havent sent any warnings to this node before
|
||||
//Remember that we sent it
|
||||
this.currentWarnings.set(nodeId, { [key]: warning });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
clearWarning(nodeId, key) {
|
||||
const warningKeys = this.currentWarnings.get(nodeId);
|
||||
|
||||
if (!warningKeys || !warningKeys[key]) {
|
||||
//There are no warnings that we've sent on this node.
|
||||
//Save some performance by not sending an uneccesary message to the editor
|
||||
return false;
|
||||
}
|
||||
|
||||
delete warningKeys[key];
|
||||
if (Object.keys(warningKeys).length === 0) {
|
||||
delete this.currentWarnings.delete(nodeId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
clearWarnings(nodeId) {
|
||||
if (this.currentWarnings.has(nodeId) === false) {
|
||||
//no warnings on this node, save some performance by not sending a message to the editor
|
||||
return false;
|
||||
}
|
||||
|
||||
//no warnings for this node anymore
|
||||
this.currentWarnings.delete(nodeId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ActiveWarnings;
|
||||
454
packages/noodl-runtime/src/editorconnection.js
Normal file
454
packages/noodl-runtime/src/editorconnection.js
Normal file
@@ -0,0 +1,454 @@
|
||||
'use strict';
|
||||
|
||||
var EventSender = require('./eventsender'),
|
||||
Services = require('./services/services'),
|
||||
guid = require('./guid');
|
||||
|
||||
const ActiveWarnings = require('./editorconnection.activewarnings');
|
||||
function EditorConnection(opts) {
|
||||
var _opts = opts || {};
|
||||
|
||||
EventSender.call(this);
|
||||
|
||||
this.runtimeType = _opts.runtimeType;
|
||||
this.platform = _opts.platform;
|
||||
this.ws =
|
||||
(_opts.platform && _opts.platform.webSocketClass) || (typeof WebSocket !== 'undefined' ? WebSocket : undefined);
|
||||
this.wsOptions = (_opts.platform && _opts.platform.webSocketOptions) || undefined;
|
||||
this.reconnectOnClose = true;
|
||||
this.enableDebugger = false;
|
||||
|
||||
this.lastSendTimestamp = 0;
|
||||
this.sendQueue = [];
|
||||
this.sendTimer = undefined;
|
||||
|
||||
//used to optimize warnings so we're not sending unneccessary warnings.
|
||||
//Clan slow down the editor in large projects
|
||||
this.activeWarnings = new ActiveWarnings();
|
||||
}
|
||||
|
||||
EditorConnection.prototype = Object.create(EventSender.prototype);
|
||||
EditorConnection.prototype.constructor = EditorConnection;
|
||||
|
||||
EditorConnection.prototype.isRunningLocally = function () {
|
||||
var runningLocallyInBrowser =
|
||||
(this.platform.isRunningLocally && this.platform.isRunningLocally()) ||
|
||||
(typeof document !== 'undefined' &&
|
||||
(document.location.hostname === 'localhost' || document.location.hostname === '127.0.0.1'));
|
||||
return runningLocallyInBrowser;
|
||||
};
|
||||
|
||||
EditorConnection.prototype.connect = function (address) {
|
||||
this.socket = this.wsOptions ? new this.ws(address, this.wsOptions) : new this.ws(address);
|
||||
|
||||
var self = this;
|
||||
|
||||
this.socket.addEventListener('open', function () {
|
||||
self.clientId = guid();
|
||||
self.socket.send(
|
||||
JSON.stringify({
|
||||
cmd: 'register',
|
||||
type: 'viewer',
|
||||
clientId: self.clientId
|
||||
})
|
||||
);
|
||||
self.emit('connected');
|
||||
});
|
||||
|
||||
this.socket.addEventListener('close', function (event) {
|
||||
if (self.reconnectOnClose) {
|
||||
self.reconnect(address);
|
||||
}
|
||||
console.log('Editor connection closed', event.code, event.reason);
|
||||
self.emit('connectionClosed');
|
||||
});
|
||||
|
||||
this.socket.addEventListener('error', function (evt) {
|
||||
console.log('Editor connection error, trying to reconnect');
|
||||
});
|
||||
|
||||
this.socket.addEventListener('message', async (e) => {
|
||||
// NOTE: When the data is too big it seems to change from string to a blob
|
||||
const text = typeof e.data === 'string' ? e.data : await e.data.text();
|
||||
const message = JSON.parse(text);
|
||||
|
||||
let content;
|
||||
|
||||
if (message.cmd === 'registered') {
|
||||
//ignore
|
||||
} else if (message.cmd === 'export') {
|
||||
content = JSON.parse(message.content);
|
||||
if (message.type === 'full' && message.target === this.clientId) {
|
||||
self.emit('exportDataFull', content);
|
||||
}
|
||||
} else if (message.cmd === 'hoverStart') {
|
||||
self.emit('hoverStart', message.content.id);
|
||||
} else if (message.cmd === 'hoverEnd') {
|
||||
self.emit('hoverEnd', message.content.id);
|
||||
} else if (message.cmd === 'refresh') {
|
||||
self.emit('reload');
|
||||
} else if (message.cmd === 'debugInspectors') {
|
||||
if (this.debugInspectorsEnabled) {
|
||||
content = JSON.parse(message.content);
|
||||
self.emit('debugInspectorsUpdated', content.inspectors);
|
||||
}
|
||||
} else if (message.cmd === 'debuggingEnabled') {
|
||||
if (self.isRunningLocally()) {
|
||||
content = JSON.parse(message.content);
|
||||
self.emit('debuggingEnabledChanged', content.enabled);
|
||||
}
|
||||
} else if (message.cmd === 'getConnectionValue') {
|
||||
if (self.isRunningLocally()) {
|
||||
content = JSON.parse(message.content);
|
||||
await self.emit('getConnectionValue', { clientId: content.clientId, connectionId: content.connectionId });
|
||||
}
|
||||
} else if (message.cmd === 'modelUpdate') {
|
||||
await self.emit('modelUpdate', message.content);
|
||||
} else if (message.cmd === 'publish') {
|
||||
Services.pubsub.routeMessage(message); // Publish a message from the pubsub service
|
||||
} else if (message.cmd === 'noodlModules') {
|
||||
self.emit('noodlModules', JSON.parse(message.content));
|
||||
} else if (message.cmd === 'mqttUpdate') {
|
||||
self.emit('mqttUpdate', message.content);
|
||||
} else if (message.cmd === 'activeComponentChanged') {
|
||||
self.emit('activeComponentChanged', message.component);
|
||||
} else {
|
||||
console.log('Command not implemented', message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.reconnect = function (address) {
|
||||
var self = this;
|
||||
|
||||
setTimeout(function () {
|
||||
self.connect(address);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
EditorConnection.prototype.isConnected = function () {
|
||||
return this.socket !== undefined && this.socket.readyState === this.ws.OPEN;
|
||||
};
|
||||
|
||||
//JSON replacer to make cyclic objects non-cyclic.
|
||||
//Using this example: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#examples
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
EditorConnection.prototype.send = function (data) {
|
||||
const now = this.platform.getCurrentTime();
|
||||
const dt = now - this.lastSendTimestamp;
|
||||
|
||||
//Send objects as json and capture exceptions
|
||||
const trySend = (msg) => {
|
||||
try {
|
||||
this.socket.send(JSON.stringify(msg));
|
||||
} catch (e) {
|
||||
if (e.message && e.message.startsWith('Converting circular')) {
|
||||
//the object is circular, try to address it
|
||||
try {
|
||||
this.socket.send(JSON.stringify(msg, getCircularReplacer()));
|
||||
} catch (e) {
|
||||
//still failed, give up
|
||||
console.log('failed to send message to editor', msg, e);
|
||||
}
|
||||
} else {
|
||||
//message couldn't be serialized to json
|
||||
console.log('failed to send message to editor', msg, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//batch messages so they're only sent at most every 200ms
|
||||
//note that the first message will always be sent immediately, and the ones for 200ms after
|
||||
//that one will be queued. So initial message response time is as low as possible (for hover etc)
|
||||
if (dt < 200 || this.sendTimer || !this.isConnected()) {
|
||||
this.sendQueue.push(data);
|
||||
if (!this.sendTimer) {
|
||||
this.sendTimer = setTimeout(() => {
|
||||
if (this.isConnected() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
//send messages in chunks. If we send too many at once the editor UI can freeze for a while
|
||||
//since it's handling these in the renderer process
|
||||
const chunkSize = 50;
|
||||
for (let i = 0; i < this.sendQueue.length; i += chunkSize) {
|
||||
const chunk = this.sendQueue.slice(i, i + chunkSize);
|
||||
trySend(chunk);
|
||||
}
|
||||
|
||||
this.sendQueue = [];
|
||||
this.sendTimer = undefined;
|
||||
this.lastSendTimestamp = this.platform.getCurrentTime();
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
this.lastSendTimestamp = now;
|
||||
trySend(data);
|
||||
}
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendInspectId = function (id) {
|
||||
this.send({
|
||||
cmd: 'select',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({ id: id })
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendSelectComponent = function (componentName) {
|
||||
this.send({
|
||||
cmd: 'select',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({ componentName })
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendPulsingConnections = function (connectionMap) {
|
||||
var connectionsToPulse = [];
|
||||
Object.keys(connectionMap).forEach(function (c) {
|
||||
var connection = connectionMap[c];
|
||||
connectionsToPulse = connectionsToPulse.concat(connection.connections);
|
||||
});
|
||||
|
||||
this.send({
|
||||
cmd: 'connectiondebugpulse',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({
|
||||
connectionsToPulse: connectionsToPulse
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendDebugInspectorValues = function (inspectors) {
|
||||
this.send({
|
||||
cmd: 'debuginspectorvalues',
|
||||
type: 'viewer',
|
||||
content: { inspectors }
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendConnectionValue = function (connectionId, value) {
|
||||
this.send({
|
||||
cmd: 'connectionValue',
|
||||
type: 'viewer',
|
||||
content: { connectionId, value }
|
||||
});
|
||||
};
|
||||
|
||||
const dynamicPortsHash = {};
|
||||
|
||||
function _detectRename(before, after) {
|
||||
if (!before || !after) return;
|
||||
|
||||
if (before.length !== after.length) return; // Must be of same length
|
||||
|
||||
var res = {};
|
||||
for (var i = 0; i < before.length; i++) {
|
||||
if (after.indexOf(before[i]) === -1) {
|
||||
if (res.before) return; // Can only be one from before that is missing
|
||||
res.before = before[i];
|
||||
}
|
||||
|
||||
if (before.indexOf(after[i]) === -1) {
|
||||
if (res.after) return; // Only one can be missing,otherwise we cannot match
|
||||
res.after = after[i];
|
||||
}
|
||||
}
|
||||
|
||||
return res.before && res.after ? res : undefined;
|
||||
}
|
||||
|
||||
EditorConnection.prototype.sendDynamicPorts = function (id, ports, options) {
|
||||
var hash = JSON.stringify(ports);
|
||||
if (dynamicPortsHash[id] === hash) {
|
||||
// Make sure we don't resend the same port data
|
||||
return;
|
||||
}
|
||||
|
||||
if (dynamicPortsHash[id] && ports && options && options.detectRenamed) {
|
||||
var detectRenamed = Array.isArray(options.detectRenamed) ? options.detectRenamed : [options.detectRenamed];
|
||||
|
||||
var renamed = [];
|
||||
detectRenamed.forEach((d) => {
|
||||
var before = JSON.parse(dynamicPortsHash[id]),
|
||||
after = [].concat(ports);
|
||||
|
||||
// Filter ports with correct prefix and plug
|
||||
if (d.prefix) {
|
||||
before = before.filter((p) => p.name.startsWith(d.prefix));
|
||||
after = after.filter((p) => p.name.startsWith(d.prefix));
|
||||
}
|
||||
|
||||
if (d.plug) {
|
||||
before = before.filter((p) => p.plug === d.plug);
|
||||
after = after.filter((p) => p.plug === d.plug);
|
||||
}
|
||||
|
||||
// Remove the prefix
|
||||
after = after.map((p) => p.name.substring((d.prefix || '').length));
|
||||
before = before.map((p) => p.name.substring((d.prefix || '').length));
|
||||
|
||||
// Find the one that is renamed (if any)
|
||||
var res = _detectRename(before, after);
|
||||
if (res) {
|
||||
renamed.push({
|
||||
plug: d.plug,
|
||||
patterns: [(d.prefix || '') + '{{*}}'],
|
||||
before: res.before,
|
||||
after: res.after
|
||||
});
|
||||
}
|
||||
});
|
||||
if (renamed.length > 0) options.renamed = renamed;
|
||||
|
||||
delete options.detectRenamed;
|
||||
}
|
||||
|
||||
dynamicPortsHash[id] = hash;
|
||||
|
||||
this.send({
|
||||
cmd: 'instanceports',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({
|
||||
nodeid: id,
|
||||
ports: ports,
|
||||
options: options
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendWarning = function (componentName, nodeId, key, warning) {
|
||||
const isNewWarning = this.activeWarnings.setWarning(nodeId, key, warning);
|
||||
|
||||
if (isNewWarning) {
|
||||
this.send({
|
||||
cmd: 'showwarning',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({
|
||||
componentName: componentName,
|
||||
nodeId: nodeId,
|
||||
key: key,
|
||||
warning: warning
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
EditorConnection.prototype.clearWarning = function (componentName, nodeId, key) {
|
||||
const hasWarning = this.activeWarnings.clearWarning(nodeId, key);
|
||||
|
||||
if (hasWarning) {
|
||||
this.send({
|
||||
cmd: 'showwarning',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({
|
||||
componentName: componentName,
|
||||
nodeId: nodeId,
|
||||
key: key,
|
||||
warning: undefined
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
EditorConnection.prototype.clearWarnings = function (componentName, nodeId) {
|
||||
const hasWarnings = this.activeWarnings.clearWarnings(nodeId);
|
||||
|
||||
if (hasWarnings) {
|
||||
this.send({
|
||||
cmd: 'clearwarnings',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({
|
||||
componentName: componentName,
|
||||
nodeId: nodeId
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendPatches = function (patches) {
|
||||
this.send({
|
||||
cmd: 'patchproject',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify(patches)
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.requestFullExport = function () {
|
||||
this.send({
|
||||
cmd: 'register',
|
||||
type: 'viewer'
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.requestNoodlModules = function () {
|
||||
this.send({
|
||||
cmd: 'getNoodlModules',
|
||||
type: 'viewer'
|
||||
});
|
||||
};
|
||||
|
||||
var serviceRequests = {};
|
||||
EditorConnection.prototype.sendServiceRequest = function (request, callback) {
|
||||
request.token = guid();
|
||||
request.clientId = this.clientId;
|
||||
serviceRequests[request.token] = callback;
|
||||
this.send(request);
|
||||
};
|
||||
|
||||
EditorConnection.prototype.close = function () {
|
||||
this.reconnectOnClose = false;
|
||||
|
||||
if (this.isConnected() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.close();
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendNodeLibrary = function (nodelibrary) {
|
||||
this.send({
|
||||
cmd: 'nodelibrary',
|
||||
type: 'viewer',
|
||||
runtimeType: this.runtimeType,
|
||||
content: nodelibrary,
|
||||
clientId: this.clientId
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendComponentMetadata = function (componentName, key, data) {
|
||||
this.send({
|
||||
cmd: 'componentMetadata',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({
|
||||
componentName,
|
||||
key,
|
||||
data
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
EditorConnection.prototype.sendProjectMetadata = function (key, data) {
|
||||
this.send({
|
||||
cmd: 'projectMetadata',
|
||||
type: 'viewer',
|
||||
content: JSON.stringify({
|
||||
key,
|
||||
data
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = EditorConnection;
|
||||
284
packages/noodl-runtime/src/editormodeleventshandler.js
Normal file
284
packages/noodl-runtime/src/editormodeleventshandler.js
Normal file
@@ -0,0 +1,284 @@
|
||||
"use strict";
|
||||
|
||||
function difference(array1, array2) {
|
||||
const valueSet = new Set(array2);
|
||||
return array1.filter(e => !valueSet.has(e));
|
||||
}
|
||||
|
||||
async function handleEvent(context, graphModel, event) {
|
||||
|
||||
function applyPortDelta(nodeModel, newPorts) {
|
||||
var inputPorts = {};
|
||||
var outputPorts = {};
|
||||
|
||||
newPorts.forEach(function(port) {
|
||||
//some ports are incorrectly named outputs instead of output, patch it here so
|
||||
//the rest of the code doesn't need to care
|
||||
if(port && port.plug === "outputs") {
|
||||
port.plug = "output";
|
||||
}
|
||||
|
||||
if(port.plug === 'input' || port.plug === 'input/output') {
|
||||
inputPorts[port.name] = port;
|
||||
}
|
||||
if(port.plug === 'output' || port.plug === 'input/output') {
|
||||
outputPorts[port.name] = port;
|
||||
}
|
||||
});
|
||||
|
||||
var existingInputs = Object.keys(nodeModel.getInputPorts());
|
||||
|
||||
var inputPortsToRemove = difference(existingInputs, Object.keys(inputPorts));
|
||||
var inputPortsToAdd = difference(Object.keys(inputPorts), existingInputs);
|
||||
|
||||
// Update port types if it has changed
|
||||
nodeModel.updateInputPortTypes(inputPorts);
|
||||
|
||||
// Remove and add input ports
|
||||
inputPortsToRemove.forEach(nodeModel.removeInputPortWithName.bind(nodeModel));
|
||||
inputPortsToAdd.forEach(function(portName) {
|
||||
nodeModel.addInputPort(inputPorts[portName]);
|
||||
if(nodeModel.parameters && nodeModel.parameters.hasOwnProperty(portName)) {
|
||||
setInputValueOnNodeInstancesWithModel(context.rootComponent.nodeScope, nodeModel, portName, nodeModel.parameters[portName]);
|
||||
}
|
||||
});
|
||||
|
||||
// Update port types if it has changed
|
||||
nodeModel.updateOutputPortTypes(outputPorts);
|
||||
|
||||
// Remove and add output ports
|
||||
var existingOutputs = Object.keys(nodeModel.getOutputPorts());
|
||||
|
||||
var outputPortsToRemove = difference(existingOutputs, Object.keys(outputPorts));
|
||||
var outputPortsToAdd = difference(Object.keys(outputPorts), existingOutputs);
|
||||
|
||||
outputPortsToRemove.forEach(nodeModel.removeOutputPortWithName.bind(nodeModel));
|
||||
outputPortsToAdd.forEach(function(portName) {
|
||||
nodeModel.addOutputPort(outputPorts[portName]);
|
||||
});
|
||||
}
|
||||
|
||||
function setInputValueOnNodeInstancesWithModel(nodeScope, nodeModel, port, value) {
|
||||
var nodes = nodeScope.getNodesWithIdRecursive(nodeModel.id);
|
||||
nodes.forEach(function(node) {
|
||||
node.queueInput(port, value);
|
||||
});
|
||||
}
|
||||
|
||||
var componentModel;
|
||||
if(event.componentName) {
|
||||
componentModel = graphModel.getComponentWithName(event.componentName);
|
||||
if(!componentModel) {
|
||||
//if we haven't received this component yet, just ignore the delta update
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//some ports are incorrectly named outputs instead of output, patch it here so
|
||||
//the rest of the code doesn't need to care
|
||||
if(event.port && event.port.plug === "outputs") {
|
||||
event.port.plug = "output";
|
||||
}
|
||||
|
||||
var eventHandlers = {
|
||||
nodeAdded: function(event) {
|
||||
componentModel.importEditorNodeData(event.model, event.parentId, event.childIndex);
|
||||
},
|
||||
nodeRemoved: async function(event) {
|
||||
if(componentModel.hasNodeWithId(event.model.id)) {
|
||||
await componentModel.removeNodeWithId(event.model.id);
|
||||
}
|
||||
},
|
||||
connectionAdded: function(event) {
|
||||
componentModel.addConnection(event.model);
|
||||
},
|
||||
connectionRemoved: function(event) {
|
||||
componentModel.removeConnection(event.model);
|
||||
|
||||
//revert to default value or parameter if this was the last connection to that port
|
||||
var targetNodeModel = componentModel.getNodeWithId(event.model.targetId);
|
||||
if(componentModel.getConnectionsToPort(targetNodeModel.id, event.model.targetId).length === 0) {
|
||||
var value = targetNodeModel.parameters[event.model.targetPort];
|
||||
if(value === undefined) {
|
||||
value = context.getDefaultValueForInput(targetNodeModel.type, event.model.targetPort);
|
||||
}
|
||||
|
||||
setInputValueOnNodeInstancesWithModel(context.rootComponent.nodeScope, targetNodeModel, event.model.targetPort, value);
|
||||
}
|
||||
|
||||
},
|
||||
parameterChanged: function(event) {
|
||||
|
||||
const nodeModel = componentModel.getNodeWithId(event.nodeId);
|
||||
if(nodeModel === undefined) {
|
||||
console.log("parameterChanged: Unknown node id",event);
|
||||
return;
|
||||
}
|
||||
|
||||
//did we get a bunch of parameters at once?
|
||||
if(event.parameters) {
|
||||
//note: some props might be deleted, then they only exist in oldParameters
|
||||
const allParams = new Set(Object.keys(event.parameters).concat(Object.keys(event.oldParameters)));
|
||||
for(const param of allParams) {
|
||||
nodeModel.setParameter(param, event.parameters[param]);
|
||||
}
|
||||
}
|
||||
|
||||
//did we get a single parameters?
|
||||
if(event.parameterName) {
|
||||
nodeModel.setParameter(event.parameterName, event.parameterValue, event.state);
|
||||
}
|
||||
},
|
||||
nodeAttached: function(event) {
|
||||
componentModel.setNodeParent(componentModel.getNodeWithId(event.nodeId), componentModel.getNodeWithId(event.parentId), event.childIndex);
|
||||
},
|
||||
nodeDetached: function(event) {
|
||||
componentModel.setNodeParent(componentModel.getNodeWithId(event.nodeId), null);
|
||||
componentModel.addRootId(event.nodeId);
|
||||
},
|
||||
componentAdded: function(event) {
|
||||
graphModel.importComponentFromEditorData(event.model);
|
||||
},
|
||||
componentRemoved: async function(event) {
|
||||
await graphModel.removeComponentWithName(event.componentName);
|
||||
},
|
||||
rootAdded: function(event) {
|
||||
componentModel.addRootId(event.nodeId);
|
||||
},
|
||||
portAdded: function(event) {
|
||||
var nodeModel = componentModel.getNodeWithId(event.nodeId);
|
||||
if(event.port.plug === "input" || event.port.plug === "input/output") {
|
||||
nodeModel.addInputPort(event.port);
|
||||
|
||||
//if node already has an old value for this port, set that value on all instances of the node
|
||||
//example: expression a+b, a=1, b=2. User removes b and then adds it again, the value 2 should be restored since it's still in the model
|
||||
if(nodeModel.parameters.hasOwnProperty(event.port)) {
|
||||
setInputValueOnNodeInstancesWithModel(context.rootComponent.nodeScope, nodeModel, event.port, nodeModel.parameters[event.port]);
|
||||
}
|
||||
}
|
||||
if(event.port.plug === "output" || event.port.plug === "input/output") {
|
||||
nodeModel.addOutputPort(event.port);
|
||||
}
|
||||
},
|
||||
portRemoved: function(event) {
|
||||
var nodeModel = componentModel.getNodeWithId(event.nodeId);
|
||||
if(event.port.plug === "input" || event.port.plug === "input/output") {
|
||||
nodeModel.removeInputPortWithName(event.port.name);
|
||||
}
|
||||
if(event.port.plug === "output" || event.port.plug === "input/output") {
|
||||
nodeModel.removeOutputPortWithName(event.port.name);
|
||||
}
|
||||
},
|
||||
nodePortRenamed: function(event) {
|
||||
if(event.port.plug === "input" || event.port.plug === "input/output") {
|
||||
componentModel.renameInputPortOnNodeWithId(event.nodeId, event.oldName, event.port.name);
|
||||
}
|
||||
if(event.port.plug === "output" || event.port.plug === "input/output") {
|
||||
componentModel.renameOutputPortOnNodeWithId(event.nodeId, event.oldName, event.port.name);
|
||||
}
|
||||
var node = componentModel.getNodeWithId(event.nodeId);
|
||||
if(node.type === "Component Inputs") {
|
||||
componentModel.addInputPort(event.port);
|
||||
graphModel.getNodesWithType(componentModel.name).forEach(function(componentInstance) {
|
||||
componentInstance.component.renameInputPortOnNodeWithId(componentInstance.id, event.oldName, event.port.name);
|
||||
});
|
||||
componentModel.removeInputPortWithName(event.oldName);
|
||||
}
|
||||
else if(node.type === "Component Outputs") {
|
||||
componentModel.addOutputPort(event.port);
|
||||
graphModel.getNodesWithType(componentModel.name).forEach(function(componentInstance) {
|
||||
componentInstance.component.renameOutputPortOnNodeWithId(componentInstance.id, event.oldName, event.port.name);
|
||||
});
|
||||
componentModel.removeOutputPortWithName(event.oldName);
|
||||
}
|
||||
},
|
||||
componentPortsUpdated: function(event) {
|
||||
applyPortDelta(componentModel, event.ports);
|
||||
},
|
||||
instancePortsChanged: function(event) {
|
||||
if(!componentModel.hasNodeWithId(event.nodeId)) return;
|
||||
var nodeModel = componentModel.getNodeWithId(event.nodeId);
|
||||
applyPortDelta(nodeModel, event.ports);
|
||||
},
|
||||
componentRenamed: function(event) {
|
||||
graphModel.renameComponent(event.oldName, event.newName);
|
||||
},
|
||||
settingsChanged: function(event) {
|
||||
graphModel.setSettings(event.settings);
|
||||
},
|
||||
metadataChanged: function(event) {
|
||||
graphModel.setMetaData(event.key,event.data);
|
||||
},
|
||||
componentMetadataChanged: function(event) {
|
||||
const c = graphModel.getComponentWithName(event.componentName);
|
||||
if(!c) return;
|
||||
c.setMetadata(event.key, event.data);
|
||||
},
|
||||
variantParametersChanged: function(event) {
|
||||
if(event.variant) {
|
||||
//we got the whole variant
|
||||
graphModel.updateVariant(event.variant);
|
||||
}
|
||||
else {
|
||||
//we got a specific value to update
|
||||
graphModel.updateVariantParameter(event.variantName, event.variantTypeName, event.parameterName, event.parameterValue, event.state);
|
||||
|
||||
//check if value has been deleted from the variant
|
||||
if(event.parameterValue === undefined) {
|
||||
//all active nodes with this variant will have to revert back to the default value, if they don't have local overrides
|
||||
const variant = graphModel.getVariant(event.variantTypeName, event.variantName);
|
||||
const nodes = context.rootComponent.nodeScope.getAllNodesWithVariantRecursive(variant);
|
||||
nodes.forEach(node => {
|
||||
node.queueInput(event.parameterName, node.getParameter(event.parameterName));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
variantDeleted: function(event) {
|
||||
graphModel.deleteVariant(event.variantTypeName, event.variantName);
|
||||
},
|
||||
variantChanged: function(event) {
|
||||
const nodeModel = componentModel.getNodeWithId(event.nodeId);
|
||||
const variant = graphModel.getVariant(nodeModel.type, event.variantName);
|
||||
|
||||
nodeModel.setVariant(variant);
|
||||
},
|
||||
variantRenamed: function(event) {
|
||||
const variant = graphModel.getVariant(event.variantTypeName, event.oldVariantName);
|
||||
if(variant) {
|
||||
variant.name = variant.variantName;
|
||||
}
|
||||
},
|
||||
defaultStateTransitionChanged: function(event) {
|
||||
const nodeModel = componentModel.getNodeWithId(event.nodeId);
|
||||
nodeModel.setDefaultStateTransition(event.curve, event.state);
|
||||
},
|
||||
stateTransitionsChanged: function(event) {
|
||||
const nodeModel = componentModel.getNodeWithId(event.nodeId);
|
||||
if(event.parameterName) {
|
||||
nodeModel.setStateTransitionParamter(event.parameterName, event.curve, event.state);
|
||||
}
|
||||
},
|
||||
variantDefaultStateTransitionChanged: function(event) {
|
||||
graphModel.updateVariantDefaultStateTransition(event.variantName, event.variantTypeName, event.curve, event.state);
|
||||
},
|
||||
variantStateTransitionsChanged: function(event) {
|
||||
graphModel.updateVariantStateTransition(event);
|
||||
},
|
||||
routerIndexChanged: function(event) {
|
||||
graphModel.routerIndex = event.data;
|
||||
}
|
||||
};
|
||||
|
||||
if(eventHandlers.hasOwnProperty(event.type)) {
|
||||
await eventHandlers[event.type](event);
|
||||
context.scheduleUpdate();
|
||||
}
|
||||
else {
|
||||
console.log('Unknown event', event);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleEvent: handleEvent
|
||||
};
|
||||
497
packages/noodl-runtime/src/events.js
Normal file
497
packages/noodl-runtime/src/events.js
Normal file
@@ -0,0 +1,497 @@
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
|
||||
var R = typeof Reflect === 'object' ? Reflect : null
|
||||
var ReflectApply = R && typeof R.apply === 'function'
|
||||
? R.apply
|
||||
: function ReflectApply(target, receiver, args) {
|
||||
return Function.prototype.apply.call(target, receiver, args);
|
||||
}
|
||||
|
||||
var ReflectOwnKeys
|
||||
if (R && typeof R.ownKeys === 'function') {
|
||||
ReflectOwnKeys = R.ownKeys
|
||||
} else if (Object.getOwnPropertySymbols) {
|
||||
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
||||
return Object.getOwnPropertyNames(target)
|
||||
.concat(Object.getOwnPropertySymbols(target));
|
||||
};
|
||||
} else {
|
||||
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
||||
return Object.getOwnPropertyNames(target);
|
||||
};
|
||||
}
|
||||
|
||||
function ProcessEmitWarning(warning) {
|
||||
if (console && console.warn) console.warn(warning);
|
||||
}
|
||||
|
||||
var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
|
||||
return value !== value;
|
||||
}
|
||||
|
||||
function EventEmitter() {
|
||||
EventEmitter.init.call(this);
|
||||
}
|
||||
module.exports = EventEmitter;
|
||||
module.exports.once = once;
|
||||
|
||||
// Backwards-compat with node 0.10.x
|
||||
EventEmitter.EventEmitter = EventEmitter;
|
||||
|
||||
EventEmitter.prototype._events = undefined;
|
||||
EventEmitter.prototype._eventsCount = 0;
|
||||
EventEmitter.prototype._maxListeners = undefined;
|
||||
|
||||
// By default EventEmitters will print a warning if more than 10 listeners are
|
||||
// added to it. This is a useful default which helps finding memory leaks.
|
||||
var defaultMaxListeners = 10;
|
||||
|
||||
function checkListener(listener) {
|
||||
if (typeof listener !== 'function') {
|
||||
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return defaultMaxListeners;
|
||||
},
|
||||
set: function(arg) {
|
||||
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
|
||||
throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
|
||||
}
|
||||
defaultMaxListeners = arg;
|
||||
}
|
||||
});
|
||||
|
||||
EventEmitter.init = function() {
|
||||
|
||||
if (this._events === undefined ||
|
||||
this._events === Object.getPrototypeOf(this)._events) {
|
||||
this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
}
|
||||
|
||||
this._maxListeners = this._maxListeners || undefined;
|
||||
};
|
||||
|
||||
// Obviously not all Emitters should be limited to 10. This function allows
|
||||
// that to be increased. Set to zero for unlimited.
|
||||
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
|
||||
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
|
||||
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
|
||||
}
|
||||
this._maxListeners = n;
|
||||
return this;
|
||||
};
|
||||
|
||||
function _getMaxListeners(that) {
|
||||
if (that._maxListeners === undefined)
|
||||
return EventEmitter.defaultMaxListeners;
|
||||
return that._maxListeners;
|
||||
}
|
||||
|
||||
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
|
||||
return _getMaxListeners(this);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.emit = function emit(type) {
|
||||
var args = [];
|
||||
for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
|
||||
var doError = (type === 'error');
|
||||
|
||||
var events = this._events;
|
||||
if (events !== undefined)
|
||||
doError = (doError && events.error === undefined);
|
||||
else if (!doError)
|
||||
return false;
|
||||
|
||||
// If there is no 'error' event listener then throw.
|
||||
if (doError) {
|
||||
var er;
|
||||
if (args.length > 0)
|
||||
er = args[0];
|
||||
if (er instanceof Error) {
|
||||
// Note: The comments on the `throw` lines are intentional, they show
|
||||
// up in Node's output if this results in an unhandled exception.
|
||||
throw er; // Unhandled 'error' event
|
||||
}
|
||||
// At least give some kind of context to the user
|
||||
var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
|
||||
err.context = er;
|
||||
throw err; // Unhandled 'error' event
|
||||
}
|
||||
|
||||
var handler = events[type];
|
||||
|
||||
if (handler === undefined)
|
||||
return false;
|
||||
|
||||
if (typeof handler === 'function') {
|
||||
ReflectApply(handler, this, args);
|
||||
} else {
|
||||
var len = handler.length;
|
||||
var listeners = arrayClone(handler, len);
|
||||
for (var i = 0; i < len; ++i)
|
||||
ReflectApply(listeners[i], this, args);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
function _addListener(target, type, listener, prepend) {
|
||||
var m;
|
||||
var events;
|
||||
var existing;
|
||||
|
||||
checkListener(listener);
|
||||
|
||||
events = target._events;
|
||||
if (events === undefined) {
|
||||
events = target._events = Object.create(null);
|
||||
target._eventsCount = 0;
|
||||
} else {
|
||||
// To avoid recursion in the case that type === "newListener"! Before
|
||||
// adding it to the listeners, first emit "newListener".
|
||||
if (events.newListener !== undefined) {
|
||||
target.emit('newListener', type,
|
||||
listener.listener ? listener.listener : listener);
|
||||
|
||||
// Re-assign `events` because a newListener handler could have caused the
|
||||
// this._events to be assigned to a new object
|
||||
events = target._events;
|
||||
}
|
||||
existing = events[type];
|
||||
}
|
||||
|
||||
if (existing === undefined) {
|
||||
// Optimize the case of one listener. Don't need the extra array object.
|
||||
existing = events[type] = listener;
|
||||
++target._eventsCount;
|
||||
} else {
|
||||
if (typeof existing === 'function') {
|
||||
// Adding the second element, need to change to array.
|
||||
existing = events[type] =
|
||||
prepend ? [listener, existing] : [existing, listener];
|
||||
// If we've already got an array, just append.
|
||||
} else if (prepend) {
|
||||
existing.unshift(listener);
|
||||
} else {
|
||||
existing.push(listener);
|
||||
}
|
||||
|
||||
// Check for listener leak
|
||||
m = _getMaxListeners(target);
|
||||
if (m > 0 && existing.length > m && !existing.warned) {
|
||||
existing.warned = true;
|
||||
// No error code for this since it is a Warning
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
var w = new Error('Possible EventEmitter memory leak detected. ' +
|
||||
existing.length + ' ' + String(type) + ' listeners ' +
|
||||
'added. Use emitter.setMaxListeners() to ' +
|
||||
'increase limit');
|
||||
w.name = 'MaxListenersExceededWarning';
|
||||
w.emitter = target;
|
||||
w.type = type;
|
||||
w.count = existing.length;
|
||||
ProcessEmitWarning(w);
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
EventEmitter.prototype.addListener = function addListener(type, listener) {
|
||||
return _addListener(this, type, listener, false);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
||||
|
||||
EventEmitter.prototype.prependListener =
|
||||
function prependListener(type, listener) {
|
||||
return _addListener(this, type, listener, true);
|
||||
};
|
||||
|
||||
function onceWrapper() {
|
||||
if (!this.fired) {
|
||||
this.target.removeListener(this.type, this.wrapFn);
|
||||
this.fired = true;
|
||||
if (arguments.length === 0)
|
||||
return this.listener.call(this.target);
|
||||
return this.listener.apply(this.target, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function _onceWrap(target, type, listener) {
|
||||
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
|
||||
var wrapped = onceWrapper.bind(state);
|
||||
wrapped.listener = listener;
|
||||
state.wrapFn = wrapped;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
EventEmitter.prototype.once = function once(type, listener) {
|
||||
checkListener(listener);
|
||||
this.on(type, _onceWrap(this, type, listener));
|
||||
return this;
|
||||
};
|
||||
|
||||
EventEmitter.prototype.prependOnceListener =
|
||||
function prependOnceListener(type, listener) {
|
||||
checkListener(listener);
|
||||
this.prependListener(type, _onceWrap(this, type, listener));
|
||||
return this;
|
||||
};
|
||||
|
||||
// Emits a 'removeListener' event if and only if the listener was removed.
|
||||
EventEmitter.prototype.removeListener =
|
||||
function removeListener(type, listener) {
|
||||
var list, events, position, i, originalListener;
|
||||
|
||||
checkListener(listener);
|
||||
|
||||
events = this._events;
|
||||
if (events === undefined)
|
||||
return this;
|
||||
|
||||
list = events[type];
|
||||
if (list === undefined)
|
||||
return this;
|
||||
|
||||
if (list === listener || list.listener === listener) {
|
||||
if (--this._eventsCount === 0)
|
||||
this._events = Object.create(null);
|
||||
else {
|
||||
delete events[type];
|
||||
if (events.removeListener)
|
||||
this.emit('removeListener', type, list.listener || listener);
|
||||
}
|
||||
} else if (typeof list !== 'function') {
|
||||
position = -1;
|
||||
|
||||
for (i = list.length - 1; i >= 0; i--) {
|
||||
if (list[i] === listener || list[i].listener === listener) {
|
||||
originalListener = list[i].listener;
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (position < 0)
|
||||
return this;
|
||||
|
||||
if (position === 0)
|
||||
list.shift();
|
||||
else {
|
||||
spliceOne(list, position);
|
||||
}
|
||||
|
||||
if (list.length === 1)
|
||||
events[type] = list[0];
|
||||
|
||||
if (events.removeListener !== undefined)
|
||||
this.emit('removeListener', type, originalListener || listener);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
||||
|
||||
EventEmitter.prototype.removeAllListeners =
|
||||
function removeAllListeners(type) {
|
||||
var listeners, events, i;
|
||||
|
||||
events = this._events;
|
||||
if (events === undefined)
|
||||
return this;
|
||||
|
||||
// not listening for removeListener, no need to emit
|
||||
if (events.removeListener === undefined) {
|
||||
if (arguments.length === 0) {
|
||||
this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
} else if (events[type] !== undefined) {
|
||||
if (--this._eventsCount === 0)
|
||||
this._events = Object.create(null);
|
||||
else
|
||||
delete events[type];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// emit removeListener for all listeners on all events
|
||||
if (arguments.length === 0) {
|
||||
var keys = Object.keys(events);
|
||||
var key;
|
||||
for (i = 0; i < keys.length; ++i) {
|
||||
key = keys[i];
|
||||
if (key === 'removeListener') continue;
|
||||
this.removeAllListeners(key);
|
||||
}
|
||||
this.removeAllListeners('removeListener');
|
||||
this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
listeners = events[type];
|
||||
|
||||
if (typeof listeners === 'function') {
|
||||
this.removeListener(type, listeners);
|
||||
} else if (listeners !== undefined) {
|
||||
// LIFO order
|
||||
for (i = listeners.length - 1; i >= 0; i--) {
|
||||
this.removeListener(type, listeners[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
function _listeners(target, type, unwrap) {
|
||||
var events = target._events;
|
||||
|
||||
if (events === undefined)
|
||||
return [];
|
||||
|
||||
var evlistener = events[type];
|
||||
if (evlistener === undefined)
|
||||
return [];
|
||||
|
||||
if (typeof evlistener === 'function')
|
||||
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
|
||||
|
||||
return unwrap ?
|
||||
unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
|
||||
}
|
||||
|
||||
EventEmitter.prototype.listeners = function listeners(type) {
|
||||
return _listeners(this, type, true);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.rawListeners = function rawListeners(type) {
|
||||
return _listeners(this, type, false);
|
||||
};
|
||||
|
||||
EventEmitter.listenerCount = function(emitter, type) {
|
||||
if (typeof emitter.listenerCount === 'function') {
|
||||
return emitter.listenerCount(type);
|
||||
} else {
|
||||
return listenerCount.call(emitter, type);
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.prototype.listenerCount = listenerCount;
|
||||
function listenerCount(type) {
|
||||
var events = this._events;
|
||||
|
||||
if (events !== undefined) {
|
||||
var evlistener = events[type];
|
||||
|
||||
if (typeof evlistener === 'function') {
|
||||
return 1;
|
||||
} else if (evlistener !== undefined) {
|
||||
return evlistener.length;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
EventEmitter.prototype.eventNames = function eventNames() {
|
||||
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
|
||||
};
|
||||
|
||||
function arrayClone(arr, n) {
|
||||
var copy = new Array(n);
|
||||
for (var i = 0; i < n; ++i)
|
||||
copy[i] = arr[i];
|
||||
return copy;
|
||||
}
|
||||
|
||||
function spliceOne(list, index) {
|
||||
for (; index + 1 < list.length; index++)
|
||||
list[index] = list[index + 1];
|
||||
list.pop();
|
||||
}
|
||||
|
||||
function unwrapListeners(arr) {
|
||||
var ret = new Array(arr.length);
|
||||
for (var i = 0; i < ret.length; ++i) {
|
||||
ret[i] = arr[i].listener || arr[i];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function once(emitter, name) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
function errorListener(err) {
|
||||
emitter.removeListener(name, resolver);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
function resolver() {
|
||||
if (typeof emitter.removeListener === 'function') {
|
||||
emitter.removeListener('error', errorListener);
|
||||
}
|
||||
resolve([].slice.call(arguments));
|
||||
};
|
||||
|
||||
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
|
||||
if (name !== 'error') {
|
||||
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
|
||||
if (typeof emitter.on === 'function') {
|
||||
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
|
||||
}
|
||||
}
|
||||
|
||||
function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
|
||||
if (typeof emitter.on === 'function') {
|
||||
if (flags.once) {
|
||||
emitter.once(name, listener);
|
||||
} else {
|
||||
emitter.on(name, listener);
|
||||
}
|
||||
} else if (typeof emitter.addEventListener === 'function') {
|
||||
// EventTarget does not have `error` event semantics like Node
|
||||
// EventEmitters, we do not listen for `error` events here.
|
||||
emitter.addEventListener(name, function wrapListener(arg) {
|
||||
// IE does not have builtin `{ once: true }` support so we
|
||||
// have to do it manually.
|
||||
if (flags.once) {
|
||||
emitter.removeEventListener(name, wrapListener);
|
||||
}
|
||||
listener(arg);
|
||||
});
|
||||
} else {
|
||||
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
|
||||
}
|
||||
}
|
||||
68
packages/noodl-runtime/src/eventsender.js
Normal file
68
packages/noodl-runtime/src/eventsender.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
function EventSender() {
|
||||
this.listeners = {};
|
||||
this.listenersWithRefs = {};
|
||||
}
|
||||
|
||||
EventSender.prototype.on = function (eventName, callback, ref) {
|
||||
if (ref) {
|
||||
if (!this.listenersWithRefs.hasOwnProperty(eventName)) {
|
||||
this.listenersWithRefs[eventName] = new Map();
|
||||
}
|
||||
|
||||
if (!this.listenersWithRefs[eventName].get(ref)) {
|
||||
this.listenersWithRefs[eventName].set(ref, []);
|
||||
}
|
||||
|
||||
this.listenersWithRefs[eventName].get(ref).push(callback);
|
||||
} else {
|
||||
if (!this.listeners.hasOwnProperty(eventName)) {
|
||||
this.listeners[eventName] = [];
|
||||
}
|
||||
|
||||
this.listeners[eventName].push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
EventSender.prototype.removeListenersWithRef = function (ref) {
|
||||
Object.keys(this.listenersWithRefs).forEach((eventName) => {
|
||||
const listeners = this.listenersWithRefs[eventName];
|
||||
if (listeners.has(ref)) {
|
||||
listeners.delete(ref);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
EventSender.prototype.removeAllListeners = function (eventName) {
|
||||
if (eventName) {
|
||||
delete this.listeners[eventName];
|
||||
delete this.listenersWithRefs[eventName];
|
||||
} else {
|
||||
this.listeners = {};
|
||||
this.listenersWithRefs = {};
|
||||
}
|
||||
};
|
||||
|
||||
EventSender.prototype.emit = async function (eventName, data) {
|
||||
const array = this.listeners[eventName];
|
||||
|
||||
if (array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const callback = array[i];
|
||||
await Promise.resolve(callback.call(null, data));
|
||||
}
|
||||
}
|
||||
|
||||
const map = this.listenersWithRefs[eventName];
|
||||
|
||||
if (map) {
|
||||
for (const [ref, callbacks] of map) {
|
||||
for (const callback of callbacks) {
|
||||
await Promise.resolve(callback.call(ref, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EventSender;
|
||||
9
packages/noodl-runtime/src/guid.js
Normal file
9
packages/noodl-runtime/src/guid.js
Normal file
@@ -0,0 +1,9 @@
|
||||
//adapted from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
|
||||
function guid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
module.exports = guid;
|
||||
458
packages/noodl-runtime/src/javascriptnodeparser.js
Normal file
458
packages/noodl-runtime/src/javascriptnodeparser.js
Normal file
@@ -0,0 +1,458 @@
|
||||
'use strict';
|
||||
|
||||
const Model = require('./model');
|
||||
const { getAbsoluteUrl } = require('./utils');
|
||||
|
||||
var userFunctionsCache = {};
|
||||
|
||||
function JavascriptNodeParser(code, options) {
|
||||
this.inputs = {};
|
||||
this.outputs = {};
|
||||
this.error = undefined;
|
||||
this.code = code;
|
||||
|
||||
const node = options ? options.node : undefined;
|
||||
this._initializeAPIs();
|
||||
|
||||
var userCode = userFunctionsCache[code];
|
||||
if (!userCode) {
|
||||
try {
|
||||
userCode = new Function(['define', 'script', 'Node', 'Component'], JavascriptNodeParser.getCodePrefix() + code);
|
||||
userFunctionsCache[code] = userCode;
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
if (userCode) {
|
||||
try {
|
||||
userCode(
|
||||
this.define.bind(this),
|
||||
this.script.bind(this),
|
||||
this.apis.Node,
|
||||
node ? JavascriptNodeParser.getComponentScopeForNode(node) : {}
|
||||
); //noodlJavascriptAPI);
|
||||
|
||||
this._afterSourced();
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First generation API
|
||||
JavascriptNodeParser.prototype.define = function (userObject) {
|
||||
this.inputs = userObject.inputs || {};
|
||||
this.outputs = userObject.outputs || {};
|
||||
this.setup = userObject.setup;
|
||||
this.change = userObject.run || userObject.change;
|
||||
this.destroy = userObject.destroy;
|
||||
|
||||
this.definedObject = userObject;
|
||||
};
|
||||
|
||||
// Second generation API
|
||||
function _scriptExtend(_this) {
|
||||
var _extended = {
|
||||
inputs: _this.inputs || {},
|
||||
outputs: _this.outputs || {},
|
||||
|
||||
setup: function (inputs, outputs) {
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
|
||||
this.setOutputs = function (states) {
|
||||
for (var key in states) {
|
||||
this.outputs[key] = states[key];
|
||||
this.flagOutputDirty(key);
|
||||
}
|
||||
};
|
||||
|
||||
if (_this.methods) {
|
||||
for (var key in _this.methods) {
|
||||
this[key] = _this.methods[key];
|
||||
}
|
||||
}
|
||||
|
||||
_this.setup && _this.setup.apply(this);
|
||||
},
|
||||
|
||||
destroy: function (inputs, outputs) {
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
|
||||
_this.destroy && _this.destroy.apply(this);
|
||||
},
|
||||
|
||||
change: function (inputs, outputs) {
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
|
||||
// Detect property changed
|
||||
var old = this._oldInputs || {};
|
||||
|
||||
if (_this.changed) {
|
||||
for (var key in inputs) {
|
||||
if (inputs[key] !== old[key]) {
|
||||
var changedFunction = _this.changed[key];
|
||||
if (typeof changedFunction === 'function') changedFunction.apply(this, [inputs[key], old[key]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._oldInputs = Object.assign({}, inputs);
|
||||
}
|
||||
};
|
||||
|
||||
if (_this.signals) {
|
||||
for (var key in _this.signals) {
|
||||
_extended[key] = _this.signals[key];
|
||||
|
||||
_extended.inputs[key] = 'signal';
|
||||
}
|
||||
}
|
||||
|
||||
return _extended;
|
||||
}
|
||||
|
||||
JavascriptNodeParser.prototype.script = function (userObject) {
|
||||
var _userObjectExtended = _scriptExtend(userObject);
|
||||
this.inputs = _userObjectExtended.inputs || {};
|
||||
this.outputs = _userObjectExtended.outputs || {};
|
||||
this.setup = _userObjectExtended.setup;
|
||||
this.change = _userObjectExtended.run || _userObjectExtended.change;
|
||||
this.destroy = _userObjectExtended.destroy;
|
||||
|
||||
this.definedObject = _userObjectExtended;
|
||||
};
|
||||
|
||||
// Third generation API
|
||||
|
||||
// Node.Setters.Hej = function(value) {...}
|
||||
// Node.OnInputsChanged = function() {...}
|
||||
// Node.Signals.Hej = function(value) {...}
|
||||
// Node.OnInit = function() {...}
|
||||
// Node.OnDestroy = function() {...}
|
||||
// Node.Inputs.A
|
||||
// Node.Outputs.A = 10;
|
||||
// Node.Outputs.Done()
|
||||
// Node.setOutputs({...})
|
||||
// Component.Object, Component.ParentObject
|
||||
JavascriptNodeParser.prototype._initializeAPIs = function () {
|
||||
this.apis = {};
|
||||
|
||||
this.apis.Node = {
|
||||
Inputs: {},
|
||||
Outputs: {},
|
||||
Signals: {},
|
||||
Setters: {}
|
||||
};
|
||||
};
|
||||
|
||||
JavascriptNodeParser.prototype._afterSourced = function () {
|
||||
if (this.definedObject !== undefined) return; // a legacy API have been used
|
||||
|
||||
var _Node = this.apis.Node;
|
||||
|
||||
// Merge inputs and outputs from node extension
|
||||
this.inputs = Object.assign({}, _Node.Inputs || {});
|
||||
this.outputs = Object.assign({}, _Node.Outputs || {});
|
||||
|
||||
this.setup = function (inputs, outputs) {
|
||||
const _this = this;
|
||||
|
||||
_Node.setOutputs = function (_outputs) {
|
||||
for (var key in _outputs) {
|
||||
outputs[key] = _outputs[key];
|
||||
_this.flagOutputDirty(key);
|
||||
}
|
||||
};
|
||||
|
||||
_Node.OnInit && _Node.OnInit.apply(this);
|
||||
};
|
||||
|
||||
this.destroy = _Node.OnDestroy || this.destory;
|
||||
this.change = (inputs, outputs, changedInputs) => {
|
||||
for (var key in changedInputs) {
|
||||
if (typeof _Node.Setters[key] === 'function') {
|
||||
_Node.Setters[key](inputs[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof _Node.OnInputsChanged === 'function') {
|
||||
_Node.OnInputsChanged();
|
||||
}
|
||||
};
|
||||
|
||||
this.definedObject = {
|
||||
inputs: this.inputs,
|
||||
outputs: this.outputs,
|
||||
setup: this.setup,
|
||||
destroy: this.destroy,
|
||||
change: this.change
|
||||
};
|
||||
|
||||
// Set all signals as signal inputs
|
||||
if (_Node.Signals !== undefined) {
|
||||
for (var key in _Node.Signals) {
|
||||
if (typeof _Node.Signals[key] === 'function') {
|
||||
this.inputs[key] = 'signal';
|
||||
|
||||
this.definedObject[key] = _Node.Signals[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JavascriptNodeParser.createFromCode = function (code, options) {
|
||||
return new JavascriptNodeParser(code, options);
|
||||
};
|
||||
|
||||
JavascriptNodeParser.createFromURL = function (url, callback, options) {
|
||||
url = getAbsoluteUrl(url);
|
||||
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
// XMLHttpRequest.DONE = 4, but torped runtime doesn't support enum
|
||||
if (this.readyState === 4 || this.readyState === XMLHttpRequest.DONE) {
|
||||
callback(new JavascriptNodeParser(this.response));
|
||||
}
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
console.log('Failed to request', url);
|
||||
};
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
JavascriptNodeParser.parseAndAddPortsFromScript = function (script, ports, options) {
|
||||
// Extract inputs and outputs
|
||||
function _exists(port) {
|
||||
if (typeof port === 'string') return ports.find((p) => p.name === port) !== undefined;
|
||||
else return ports.find((p) => p.name === port.name && p.plug === port.plug) !== undefined;
|
||||
}
|
||||
|
||||
function _addPortsFromMatch(match, options) {
|
||||
if (match === undefined || match === null) return;
|
||||
|
||||
const unique = {};
|
||||
for (const _s of match) {
|
||||
let name = _s[1];
|
||||
if (name === undefined) continue;
|
||||
|
||||
unique[name] = true;
|
||||
}
|
||||
|
||||
Object.keys(unique).forEach((p) => {
|
||||
if (
|
||||
_exists({
|
||||
name: options.prefix + p,
|
||||
plug: options.plug
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
ports.push({
|
||||
name: options.prefix + p,
|
||||
displayName: p,
|
||||
plug: options.plug,
|
||||
type: options.type,
|
||||
group: options.group
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// const scriptWithoutComments = script.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,''); // Remove comments
|
||||
|
||||
// Regular Inputs. notation
|
||||
if (!options.skipInputs) {
|
||||
_addPortsFromMatch(script.matchAll(/Inputs\.([A-Za-z0-9_]+)/g), {
|
||||
type: options.inputType || '*',
|
||||
plug: 'input',
|
||||
group: options.inputGroup || 'Inputs',
|
||||
prefix: options.inputPrefix || ''
|
||||
});
|
||||
|
||||
// Inputs with Inputs["A"] notation
|
||||
_addPortsFromMatch(script.matchAll(/Inputs\s*\[\s*(?:'|")(.*)(?:'|")\s*\]/g), {
|
||||
type: options.inputType || '*',
|
||||
plug: 'inputs',
|
||||
group: options.inputGroup || 'Inputs',
|
||||
prefix: options.inputPrefix || ''
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.skipOutputs) {
|
||||
if (!options.skipOutputSignals) {
|
||||
// Output signals, Output.Done()
|
||||
_addPortsFromMatch(script.matchAll(/Outputs\.([A-Za-z0-9]+)\s*\(\s*\)/g), {
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Outputs',
|
||||
prefix: options.outputPrefix || ''
|
||||
});
|
||||
|
||||
// Output signals, Outputs["Done"]()
|
||||
_addPortsFromMatch(script.matchAll(/Outputs\s*\[\s*(?:'|")(.*)(?:'|")\s*\]\(\s*\)/g), {
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Outputs',
|
||||
prefix: options.outputPrefix || ''
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.skipRegularOutputs) {
|
||||
// Regular output Outputs. notation
|
||||
_addPortsFromMatch(script.matchAll(/Outputs\.([A-Za-z0-9_]+)/g), {
|
||||
type: '*',
|
||||
plug: 'output',
|
||||
group: 'Outputs',
|
||||
prefix: options.outputPrefix || ''
|
||||
});
|
||||
|
||||
// Outputs with Outputs["A"] notation
|
||||
_addPortsFromMatch(script.matchAll(/Outputs\s*\[\s*\"([^\"]*)\"\s*\]/g), {
|
||||
type: '*',
|
||||
plug: 'output',
|
||||
group: 'Outputs',
|
||||
prefix: options.outputPrefix || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JavascriptNodeParser.prototype.getPorts = function () {
|
||||
var ports = [];
|
||||
|
||||
var self = this;
|
||||
|
||||
Object.keys(this.inputs).forEach(function (name) {
|
||||
var inputPort = self.inputs[name];
|
||||
|
||||
var port = {
|
||||
name: name,
|
||||
plug: 'input'
|
||||
};
|
||||
if (typeof inputPort === 'string') {
|
||||
port.type = {
|
||||
name: inputPort
|
||||
};
|
||||
port.group = 'Inputs';
|
||||
} else {
|
||||
for (var p in inputPort) {
|
||||
port[p] = inputPort[p];
|
||||
}
|
||||
}
|
||||
|
||||
ports.push(port);
|
||||
});
|
||||
|
||||
Object.keys(this.outputs).forEach(function (name) {
|
||||
ports.push({
|
||||
name: name,
|
||||
type: {
|
||||
name: self.outputs[name]
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Outputs'
|
||||
});
|
||||
});
|
||||
|
||||
JavascriptNodeParser.parseAndAddPortsFromScript(this.code, ports, {});
|
||||
|
||||
return ports;
|
||||
};
|
||||
|
||||
const _componentScopes = {};
|
||||
|
||||
function _findParentComponentStateModelId(node) {
|
||||
function getParentComponent(component) {
|
||||
let parent;
|
||||
if (component.getRoots().length > 0) {
|
||||
//visual
|
||||
const root = component.getRoots()[0];
|
||||
|
||||
if (root.getVisualParentNode) {
|
||||
//regular visual node
|
||||
if (root.getVisualParentNode()) {
|
||||
parent = root.getVisualParentNode().nodeScope.componentOwner;
|
||||
}
|
||||
} else if (root.parentNodeScope) {
|
||||
//component instance node
|
||||
parent = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
} else if (component.parentNodeScope) {
|
||||
parent = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
|
||||
//check that a parent exists and that the component is different
|
||||
if (parent && parent.nodeScope && parent.nodeScope.componentOwner !== component) {
|
||||
//check if parent has a Component State node
|
||||
if (parent.nodeScope.getNodesWithType('net.noodl.ComponentObject').length > 0) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
if (parent.nodeScope.getNodesWithType('Component State').length > 0) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
//if not, continue searching up the tree
|
||||
return getParentComponent(parent);
|
||||
}
|
||||
}
|
||||
|
||||
const parent = getParentComponent(node.nodeScope.componentOwner);
|
||||
if (!parent) return;
|
||||
|
||||
//this._internal.parentComponentName = parent.name;
|
||||
|
||||
return 'componentState' + parent.getInstanceId();
|
||||
}
|
||||
|
||||
function _findForEachModel(node) {
|
||||
var component = node.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
return component !== undefined ? component._forEachModel : undefined;
|
||||
}
|
||||
|
||||
JavascriptNodeParser.getComponentScopeForNode = function (node) {
|
||||
const componentId = node.nodeScope.componentOwner.getInstanceId();
|
||||
|
||||
if (_componentScopes[componentId] === undefined) {
|
||||
_componentScopes[componentId] = {};
|
||||
|
||||
const componentObject = (node.nodeScope.modelScope || Model).get('componentState' + componentId);
|
||||
|
||||
_componentScopes[componentId].Object = componentObject;
|
||||
}
|
||||
|
||||
// This should be done each time, since the component can be moved
|
||||
const parentComponentObjectId = _findParentComponentStateModelId(node);
|
||||
const parentComponentObject =
|
||||
parentComponentObjectId !== undefined
|
||||
? (node.nodeScope.modelScope || Model).get(parentComponentObjectId)
|
||||
: undefined;
|
||||
|
||||
_componentScopes[componentId].ParentObject = parentComponentObject;
|
||||
|
||||
// Set the for each model
|
||||
_componentScopes[componentId].RepeaterObject = _findForEachModel(node);
|
||||
|
||||
return _componentScopes[componentId];
|
||||
};
|
||||
|
||||
JavascriptNodeParser.getCodePrefix = function () {
|
||||
// API
|
||||
return "const Script = (typeof Node !== 'undefined')?Node:undefined;\n";
|
||||
};
|
||||
|
||||
JavascriptNodeParser.createNoodlAPI = function () {
|
||||
// If we are running in browser mode and there is a global Noodl API object, use it. If not
|
||||
// create a new one for this scope.
|
||||
return typeof window !== 'undefined' && window.Noodl !== undefined ? window.Noodl : {};
|
||||
};
|
||||
|
||||
module.exports = JavascriptNodeParser;
|
||||
215
packages/noodl-runtime/src/model.js
Normal file
215
packages/noodl-runtime/src/model.js
Normal file
@@ -0,0 +1,215 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// Model
|
||||
// ------------------------------------------------------------------------------
|
||||
function Model(id, data) {
|
||||
this.id = id;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
const models = (Model._models = {});
|
||||
const proxies = {};
|
||||
|
||||
// Get and set proxy
|
||||
const _modelProxyHandler = {
|
||||
get: function (target, prop, receiver) {
|
||||
if (typeof target[prop] === 'function') return target[prop].bind(target);
|
||||
else if (prop in target) return Reflect.get(target, prop, receiver);
|
||||
else return target.get(prop);
|
||||
},
|
||||
set: function (obj, prop, value) {
|
||||
if (prop === '_class') {
|
||||
obj._class = value;
|
||||
} else if (prop === 'id') {
|
||||
console.log(`Noodl.Object warning: id is readonly (Id is ${obj.id}, trying to set to ${value})`);
|
||||
return true; //if a proxy doesn't return true an exception will be thrown
|
||||
} else {
|
||||
obj.set(prop, value);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
ownKeys(target) {
|
||||
return Reflect.ownKeys(target.data);
|
||||
},
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return Object.getOwnPropertyDescriptor(target.data, prop);
|
||||
}
|
||||
};
|
||||
|
||||
Model.get = function (id) {
|
||||
if (id === undefined) id = Model.guid();
|
||||
if (!models[id]) {
|
||||
models[id] = new Model(id, {});
|
||||
proxies[id] = new Proxy(models[id], _modelProxyHandler);
|
||||
}
|
||||
return proxies[id];
|
||||
};
|
||||
|
||||
Model.create = function (data) {
|
||||
var modelData = data ? data : {};
|
||||
var m = Model.get(modelData.id);
|
||||
for (var key in modelData) {
|
||||
if (key === 'id') continue;
|
||||
m.set(key, modelData[key]);
|
||||
}
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
Model.exists = function (id) {
|
||||
return models[id] !== undefined;
|
||||
};
|
||||
|
||||
Model.instanceOf = function (collection) {
|
||||
return collection instanceof Model || collection.target instanceof Model;
|
||||
};
|
||||
|
||||
function _randomString(size) {
|
||||
if (size === 0) {
|
||||
throw new Error('Zero-length randomString is useless.');
|
||||
}
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789';
|
||||
let objectId = '';
|
||||
for (let i = 0; i < size; ++i) {
|
||||
objectId += chars[Math.floor((1 + Math.random()) * 0x10000) % chars.length];
|
||||
}
|
||||
return objectId;
|
||||
}
|
||||
|
||||
Model.guid = function guid() {
|
||||
return _randomString(10);
|
||||
};
|
||||
|
||||
Model.prototype.on = function (event, listener) {
|
||||
if (!this.listeners) this.listeners = {};
|
||||
if (!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(listener);
|
||||
};
|
||||
|
||||
Model.prototype.off = function (event, listener) {
|
||||
if (!this.listeners) return;
|
||||
if (!this.listeners[event]) return;
|
||||
var idx = this.listeners[event].indexOf(listener);
|
||||
if (idx !== -1) this.listeners[event].splice(idx, 1);
|
||||
};
|
||||
|
||||
Model.prototype.notify = function (event, args) {
|
||||
if (!this.listeners) return;
|
||||
if (!this.listeners[event]) return;
|
||||
|
||||
var l = this.listeners[event].slice(); //clone in case listeners array is modified in the callbacks
|
||||
for (var i = 0; i < l.length; i++) {
|
||||
l[i](args);
|
||||
}
|
||||
};
|
||||
|
||||
Model.prototype.setAll = function (obj) {
|
||||
for (var i in obj) {
|
||||
if (i === 'id') continue; // Skip id
|
||||
if (this.data[i] !== obj[i]) {
|
||||
var old = this.data[i];
|
||||
this.data[i] = obj[i];
|
||||
this.notify('change', { name: i, value: obj[i], old: old });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Model.prototype.fill = function (value = null) {
|
||||
for (const key in this.data) {
|
||||
if (key === 'id') continue; // Skip id
|
||||
const temp = this.data[key];
|
||||
this.data[key] = value;
|
||||
this.notify('change', { name: key, value: this.data[key], old: temp });
|
||||
}
|
||||
};
|
||||
|
||||
Model.prototype.set = function (name, value, args) {
|
||||
if (args && args.resolve && name.indexOf('.') !== -1) {
|
||||
// We should resolve path references
|
||||
var path = name.split('.');
|
||||
var model = this;
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
var v = model.get(path[i]);
|
||||
if (Model.instanceOf(v)) model = v;
|
||||
else return; // Path resolve failed
|
||||
}
|
||||
model.set(path[path.length - 1], value);
|
||||
return;
|
||||
}
|
||||
|
||||
const forceChange = args && args.forceChange;
|
||||
|
||||
var oldValue = this.data[name];
|
||||
this.data[name] = value;
|
||||
(forceChange || oldValue !== value) &&
|
||||
(!args || !args.silent) &&
|
||||
this.notify('change', { name: name, value: value, old: oldValue });
|
||||
};
|
||||
|
||||
Model.prototype.getId = function () {
|
||||
return this.id;
|
||||
};
|
||||
|
||||
Model.prototype.get = function (name, args) {
|
||||
if (args && args.resolve && name.indexOf('.') !== -1) {
|
||||
// We should resolve path references
|
||||
var path = name.split('.');
|
||||
var model = this;
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
var v = model.get(path[i]);
|
||||
if (Model.instanceOf(v)) model = v;
|
||||
else return; // Path resolve failed
|
||||
}
|
||||
return model.get(path[path.length - 1]);
|
||||
}
|
||||
|
||||
return this.data[name];
|
||||
};
|
||||
|
||||
Model.prototype.toJSON = function () {
|
||||
return Object.assign({}, this.data, { id: this.id });
|
||||
};
|
||||
|
||||
Model.Scope = function () {
|
||||
this.models = {};
|
||||
this.proxies = {};
|
||||
};
|
||||
|
||||
Model.Scope.prototype.get = function (id) {
|
||||
if (id === undefined) id = Model.guid();
|
||||
if (!this.models[id]) {
|
||||
this.models[id] = new Model(id, {});
|
||||
this.proxies[id] = new Proxy(this.models[id], _modelProxyHandler);
|
||||
}
|
||||
return this.proxies[id];
|
||||
};
|
||||
|
||||
Model.Scope.prototype.create = function (data) {
|
||||
var modelData = data ? data : {};
|
||||
var m = this.get(modelData.id);
|
||||
for (var key in modelData) {
|
||||
if (key === 'id') continue;
|
||||
m.set(key, modelData[key]);
|
||||
}
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
Model.Scope.prototype.exists = function (id) {
|
||||
return this.models[id] !== undefined;
|
||||
};
|
||||
|
||||
Model.Scope.prototype.instanceOf = function (collection) {
|
||||
return collection instanceof Model || collection.target instanceof Model;
|
||||
};
|
||||
|
||||
Model.Scope.prototype.guid = function guid() {
|
||||
return _randomString(10);
|
||||
};
|
||||
|
||||
Model.Scope.prototype.reset = function () {
|
||||
this.models = {};
|
||||
this.proxies = {};
|
||||
delete this._cloudStore;
|
||||
};
|
||||
|
||||
module.exports = Model;
|
||||
389
packages/noodl-runtime/src/models/componentmodel.js
Normal file
389
packages/noodl-runtime/src/models/componentmodel.js
Normal file
@@ -0,0 +1,389 @@
|
||||
"use strict";
|
||||
|
||||
var NodeModel = require('./nodemodel');
|
||||
var EventSender = require('../eventsender');
|
||||
|
||||
function ComponentModel(name) {
|
||||
EventSender.call(this);
|
||||
|
||||
this.name = name;
|
||||
this.nodes = [];
|
||||
this.connections = [];
|
||||
this.roots = [];
|
||||
this.inputPorts = {};
|
||||
this.outputPorts = {};
|
||||
this.metadata = {};
|
||||
}
|
||||
|
||||
ComponentModel.prototype = Object.create(EventSender.prototype);
|
||||
|
||||
ComponentModel.prototype.addNode = async function(node) {
|
||||
node.component = this;
|
||||
this.nodes[node.id] = node;
|
||||
await this.emit("nodeAdded", node);
|
||||
};
|
||||
|
||||
ComponentModel.prototype.hasNodeWithId = function(id) {
|
||||
return this.getNodeWithId(id) !== undefined;
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getNodeWithId = function(id) {
|
||||
return this.nodes[id];
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getAllNodes = function() {
|
||||
return Object.values(this.nodes);
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getNodesWithType = function(type) {
|
||||
var nodes = [];
|
||||
var self = this;
|
||||
Object.keys(this.nodes).forEach(function(id) {
|
||||
var node = self.nodes[id];
|
||||
if(node.type === type) {
|
||||
nodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
||||
ComponentModel.prototype.addConnection = function(connection) {
|
||||
this.connections.push(connection);
|
||||
this.emit("connectionAdded", connection);
|
||||
|
||||
//emit an event on the target node model
|
||||
//used by numbered inputs
|
||||
if(connection.targetId) {
|
||||
const node = this.getNodeWithId(connection.targetId);
|
||||
if(node) {
|
||||
node.emit("inputConnectionAdded", connection);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.removeConnection = function(connection) {
|
||||
const index = this.connections.findIndex(con => {
|
||||
return con.sourceId === connection.sourceId &&
|
||||
con.sourcePort === connection.sourcePort &&
|
||||
con.targetId === connection.targetId &&
|
||||
con.targetPort === connection.targetPort;
|
||||
});
|
||||
|
||||
if(index === -1) {
|
||||
console.log("Connection doesn't exist", connection);
|
||||
return;
|
||||
}
|
||||
|
||||
this.connections.splice(index, 1);
|
||||
this.emit("connectionRemoved", connection);
|
||||
|
||||
//emit an event on the target node model
|
||||
//used by numbered inputs
|
||||
if(connection.targetId) {
|
||||
const node = this.getNodeWithId(connection.targetId);
|
||||
if(node) {
|
||||
node.emit("inputConnectionRemoved", connection);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getConnectionsFromPort = function(nodeId, sourcePortName) {
|
||||
return this.connections.filter(function(connection) {
|
||||
return connection.sourceId === nodeId && connection.sourcePort === sourcePortName;
|
||||
})
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getConnectionsToPort = function(nodeId, targetPortName) {
|
||||
return this.connections.filter(function(connection) {
|
||||
return connection.targetId === nodeId && connection.targetPort === targetPortName;
|
||||
})
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getConnectionsFrom = function(nodeId) {
|
||||
return this.connections.filter(function(connection) {
|
||||
return connection.sourceId === nodeId;
|
||||
})
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getConnectionsTo = function(nodeId) {
|
||||
return this.connections.filter(function(connection) {
|
||||
return connection.targetId === nodeId;
|
||||
})
|
||||
};
|
||||
|
||||
ComponentModel.prototype.addRootId = function(rootId) {
|
||||
if(this.roots.indexOf(rootId) !== -1) {
|
||||
return;
|
||||
}
|
||||
this.roots.push(rootId);
|
||||
this.emit("rootAdded", rootId);
|
||||
};
|
||||
|
||||
ComponentModel.prototype.removeRootId = function(rootId) {
|
||||
const index = this.roots.indexOf(rootId);
|
||||
if(index !== -1) {
|
||||
this.roots.splice(index, 1);
|
||||
this.emit("rootRemoved", rootId);
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getRoots = function() {
|
||||
return this.roots;
|
||||
}
|
||||
|
||||
ComponentModel.prototype.removeNodeWithId = async function(id) {
|
||||
const node = this.getNodeWithId(id);
|
||||
|
||||
if (!node) {
|
||||
console.warn("ERROR: Attempted to remove non-existing node with ID:", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
//remove children first
|
||||
while(node.children.length > 0) {
|
||||
const child = node.children[0];
|
||||
const childRemoved = await this.removeNodeWithId(child.id);
|
||||
if (!childRemoved) {
|
||||
// Workaround for corrupt node trees, should never happen, a remove should always be successful
|
||||
node.children.shift();
|
||||
}
|
||||
}
|
||||
|
||||
const connections = this.getConnectionsTo(id).concat(this.getConnectionsFrom(id));
|
||||
|
||||
for(let i=0; i<connections.length; i++) {
|
||||
this.removeConnection(connections[i]);
|
||||
}
|
||||
|
||||
this.setNodeParent(node, null);
|
||||
|
||||
if(this.roots.indexOf(node.id) !== -1) {
|
||||
this.removeRootId(node.id);
|
||||
}
|
||||
|
||||
await this.emit("nodeRemoved", node);
|
||||
|
||||
node.removeAllListeners();
|
||||
delete this.nodes[id];
|
||||
|
||||
await this.emit("nodeWasRemoved", node);
|
||||
return true;
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getAllConnections = function() {
|
||||
return this.connections;
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getInputPorts = function() {
|
||||
return this.inputPorts;
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getOutputPorts = function() {
|
||||
return this.outputPorts;
|
||||
};
|
||||
|
||||
ComponentModel.prototype.addInputPort = function(port) {
|
||||
this.inputPorts[port.name] = port;
|
||||
this.emit("inputPortAdded", port);
|
||||
};
|
||||
|
||||
ComponentModel.prototype.addOutputPort = function(port) {
|
||||
this.outputPorts[port.name] = port;
|
||||
this.emit("outputPortAdded", port);
|
||||
};
|
||||
|
||||
ComponentModel.prototype.removeOutputPortWithName = function(portName) {
|
||||
if(this.outputPorts.hasOwnProperty(portName)) {
|
||||
var port = this.outputPorts[portName];
|
||||
delete this.outputPorts[portName];
|
||||
this.emit("outputPortRemoved", port);
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.removeInputPortWithName = function(portName) {
|
||||
if(this.inputPorts.hasOwnProperty(portName)) {
|
||||
var port = this.inputPorts[portName];
|
||||
delete this.inputPorts[portName];
|
||||
this.emit("inputPortRemoved", port);
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.updateInputPortTypes = function(ports) {
|
||||
var changed = false;
|
||||
for(var key in ports) {
|
||||
if(this.inputPorts[key] !== undefined) {
|
||||
this.inputPorts[key].type = ports[key].type;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed && this.emit("inputPortTypesUpdated");
|
||||
}
|
||||
|
||||
ComponentModel.prototype.updateOutputPortTypes = function(ports) {
|
||||
var changed = false;
|
||||
for(var key in ports) {
|
||||
if(this.outputPorts[key] !== undefined) {
|
||||
this.outputPorts[key].type = ports[key].type;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed && this.emit("outputPortTypesUpdated");
|
||||
}
|
||||
|
||||
ComponentModel.prototype.renameInputPortOnNodeWithId = function(id, oldName, newName) {
|
||||
|
||||
//remove connections
|
||||
var connections = this.getConnectionsToPort(id, oldName);
|
||||
connections.forEach(this.removeConnection.bind(this));
|
||||
|
||||
//get port before deleting it
|
||||
var nodeModel = this.getNodeWithId(id);
|
||||
var port = {...nodeModel.getInputPort(oldName)};
|
||||
|
||||
//remove old port
|
||||
if(port) {
|
||||
nodeModel.removeInputPortWithName(oldName);
|
||||
|
||||
//rename and add new port
|
||||
port.name = newName;
|
||||
nodeModel.addInputPort(port);
|
||||
}
|
||||
|
||||
//add new connection
|
||||
connections.forEach(function(connection) {
|
||||
connection.targetPort = newName;
|
||||
});
|
||||
|
||||
connections.forEach(this.addConnection.bind(this));
|
||||
};
|
||||
|
||||
ComponentModel.prototype.renameOutputPortOnNodeWithId = function(id, oldName, newName) {
|
||||
|
||||
//remove connections
|
||||
var connections = this.getConnectionsFromPort(id, oldName);
|
||||
connections.forEach(this.removeConnection.bind(this));
|
||||
|
||||
//get port before deleting it
|
||||
var nodeModel = this.getNodeWithId(id);
|
||||
var port = {...nodeModel.getOutputPort(oldName)};
|
||||
|
||||
//remove old port
|
||||
nodeModel.removeOutputPortWithName(oldName);
|
||||
|
||||
//rename and add new port
|
||||
port.name = newName;
|
||||
nodeModel.addOutputPort(port);
|
||||
|
||||
//add new connection
|
||||
connections.forEach(function(connection) {
|
||||
connection.sourcePort = newName;
|
||||
});
|
||||
|
||||
connections.forEach(this.addConnection.bind(this));
|
||||
};
|
||||
|
||||
ComponentModel.prototype.setNodeParent = function(childModel, newParentModel, index) {
|
||||
|
||||
if(this.roots.indexOf(childModel.id) !== -1) {
|
||||
this.removeRootId(childModel.id)
|
||||
}
|
||||
|
||||
if(childModel.parent) {
|
||||
this.emit("nodeParentWillBeRemoved", childModel);
|
||||
childModel.parent.removeChild(childModel);
|
||||
}
|
||||
childModel.emit("parentUpdated", newParentModel);
|
||||
if(newParentModel) {
|
||||
newParentModel.addChild(childModel, index);
|
||||
this.emit("nodeParentUpdated", childModel);
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.importEditorNodeData = async function(nodeData, parentId, childIndex) {
|
||||
var nodeModel = NodeModel.createFromExportData(nodeData);
|
||||
await this.addNode(nodeModel);
|
||||
|
||||
if(parentId) {
|
||||
this.setNodeParent(nodeModel, this.getNodeWithId(parentId), childIndex);
|
||||
}
|
||||
|
||||
if(nodeData.children) {
|
||||
for(let i=0; i<nodeData.children.length; i++) {
|
||||
const child = nodeData.children[i];
|
||||
await this.importEditorNodeData(child, nodeModel.id, i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.reset = async function() {
|
||||
while(this.roots.length) {
|
||||
await this.removeNodeWithId(this.roots[0]);
|
||||
}
|
||||
|
||||
for(const id of this.nodes) {
|
||||
//note: with an incomplete library there will be no roots
|
||||
//so some of the nodes will have children, which will be recursively removed by
|
||||
//removeNodeWithId(), so some IDs from the Object.keys(this.nodes) that runs this loop
|
||||
//will already have been removed, so check if they exist before removing
|
||||
if(this.hasNodeWithId(id)) {
|
||||
await this.removeNodeWithId(id);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.nodes.length > 0) {
|
||||
throw new Error("Not all nodes were removed during a reset");
|
||||
}
|
||||
|
||||
if(this.connections.length > 0) {
|
||||
throw new Error("Not all connections were removed during a reset");
|
||||
}
|
||||
};
|
||||
|
||||
ComponentModel.prototype.rename = function(newName) {
|
||||
var oldName = this.name;
|
||||
this.name = newName;
|
||||
this.emit("renamed", {oldName: oldName, newName: newName});
|
||||
};
|
||||
|
||||
ComponentModel.prototype.setMetadata = function(key, data) {
|
||||
this.metadata[key] = data;
|
||||
};
|
||||
|
||||
ComponentModel.prototype.getMetadata = function(key) {
|
||||
if(!key) return this.metadata;
|
||||
return this.metadata[key];
|
||||
};
|
||||
|
||||
ComponentModel.createFromExportData = async function(componentData) {
|
||||
|
||||
var componentModel = new ComponentModel(componentData.name);
|
||||
|
||||
if(componentData.metadata) {
|
||||
for(const key in componentData.metadata) {
|
||||
componentModel.setMetadata(key, componentData.metadata[key]);
|
||||
}
|
||||
}
|
||||
|
||||
componentData.ports && componentData.ports.forEach(function(port) {
|
||||
if(port.plug === "input" || port.plug === "input/output") {
|
||||
componentModel.addInputPort(port);
|
||||
}
|
||||
if(port.plug === "output" || port.plug === "input/output") {
|
||||
componentModel.addOutputPort(port);
|
||||
}
|
||||
});
|
||||
|
||||
if(componentData.nodes) {
|
||||
for(const node of componentData.nodes) {
|
||||
await componentModel.importEditorNodeData(node);
|
||||
}
|
||||
}
|
||||
|
||||
componentData.connections && componentData.connections.forEach(connection => componentModel.addConnection(connection));
|
||||
componentData.roots && componentData.roots.forEach(root => componentModel.addRootId(root));
|
||||
|
||||
return componentModel;
|
||||
};
|
||||
|
||||
module.exports = ComponentModel;
|
||||
337
packages/noodl-runtime/src/models/graphmodel.js
Normal file
337
packages/noodl-runtime/src/models/graphmodel.js
Normal file
@@ -0,0 +1,337 @@
|
||||
'use strict';
|
||||
|
||||
var ComponentModel = require('./componentmodel');
|
||||
var EventSender = require('../eventsender');
|
||||
|
||||
function GraphModel() {
|
||||
EventSender.call(this);
|
||||
this.components = {};
|
||||
|
||||
this.settings = {};
|
||||
|
||||
this.metadata = {};
|
||||
}
|
||||
|
||||
GraphModel.prototype = Object.create(EventSender.prototype);
|
||||
|
||||
GraphModel.prototype.importComponentFromEditorData = async function (componentData) {
|
||||
var componentModel = await ComponentModel.createFromExportData(componentData);
|
||||
this.addComponent(componentModel);
|
||||
};
|
||||
|
||||
GraphModel.prototype.getBundleContainingComponent = function (name) {
|
||||
return this.componentToBundleMap.get(name);
|
||||
};
|
||||
|
||||
GraphModel.prototype.getBundlesContainingSheet = function (sheetName) {
|
||||
const bundles = new Set();
|
||||
for (const name of this.componentToBundleMap.keys()) {
|
||||
const isOnDefaultSheet = name.indexOf('/#') !== 0;
|
||||
|
||||
const isMatch =
|
||||
(isOnDefaultSheet && sheetName === 'Default') || (!isOnDefaultSheet && name.indexOf('/#' + sheetName) === 0);
|
||||
|
||||
if (isMatch) {
|
||||
bundles.add(this.componentToBundleMap.get(name));
|
||||
}
|
||||
}
|
||||
return Array.from(bundles);
|
||||
};
|
||||
|
||||
GraphModel.prototype.getBundleDependencies = function (bundleName) {
|
||||
const result = new Set();
|
||||
|
||||
const recurse = (name) => {
|
||||
const bundle = this.componentIndex[name];
|
||||
for (const dep of bundle.dependencies) {
|
||||
if (!result.has(dep)) {
|
||||
result.add(dep);
|
||||
recurse(dep);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
recurse(bundleName);
|
||||
|
||||
return Array.from(result);
|
||||
};
|
||||
|
||||
GraphModel.prototype.importEditorData = async function (exportData) {
|
||||
this.componentIndex = exportData.componentIndex;
|
||||
this.routerIndex = exportData.routerIndex;
|
||||
|
||||
this.componentToBundleMap = new Map();
|
||||
|
||||
for (const bundleName in exportData.componentIndex) {
|
||||
const bundle = exportData.componentIndex[bundleName];
|
||||
|
||||
for (const componentName of bundle.components) {
|
||||
this.componentToBundleMap.set(componentName, bundleName);
|
||||
}
|
||||
}
|
||||
|
||||
this.variants = exportData.variants || [];
|
||||
|
||||
exportData.settings && this.setSettings(exportData.settings);
|
||||
|
||||
exportData.metadata && this.setAllMetaData(exportData.metadata);
|
||||
|
||||
for (const component of exportData.components) {
|
||||
await this.importComponentFromEditorData(component);
|
||||
}
|
||||
|
||||
this.setRootComponentName(exportData.rootComponent);
|
||||
};
|
||||
|
||||
GraphModel.prototype.setRootComponentName = function (componentName) {
|
||||
this.rootComponent = componentName;
|
||||
this.emit('rootComponentNameUpdated', componentName);
|
||||
};
|
||||
|
||||
GraphModel.prototype.getNodesWithType = function (type) {
|
||||
var nodes = [];
|
||||
|
||||
var componentNames = Object.keys(this.components);
|
||||
for (var i = 0; i < componentNames.length; i++) {
|
||||
var component = this.components[componentNames[i]];
|
||||
nodes = nodes.concat(component.getNodesWithType(type));
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
|
||||
GraphModel.prototype.getComponentWithName = function (type) {
|
||||
return this.components[type];
|
||||
};
|
||||
|
||||
GraphModel.prototype.hasComponentWithName = function (type) {
|
||||
return this.components[type] ? true : false;
|
||||
};
|
||||
|
||||
GraphModel.prototype.getAllComponents = function () {
|
||||
return Object.keys(this.components).map((name) => {
|
||||
return this.components[name];
|
||||
});
|
||||
};
|
||||
|
||||
GraphModel.prototype.getAllNodes = function () {
|
||||
var nodes = [];
|
||||
|
||||
var componentNames = Object.keys(this.components);
|
||||
for (var i = 0; i < componentNames.length; i++) {
|
||||
var component = this.components[componentNames[i]];
|
||||
nodes = nodes.concat(component.getAllNodes());
|
||||
}
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
||||
GraphModel.prototype.addComponent = function (component) {
|
||||
this.components[component.name] = component;
|
||||
|
||||
//nodes that are already added are missing component input/output ports if the component is registered after the nodes
|
||||
//now when we have the component info, add them to the node instance models
|
||||
this.getNodesWithType(component.name).forEach(this._addComponentPorts.bind(this));
|
||||
|
||||
//emit the "nodeAdded" event for every node already in the component
|
||||
component.getAllNodes().forEach(this._onNodeAdded.bind(this));
|
||||
|
||||
//emit the same event for future nodes that will be added
|
||||
component.on('nodeAdded', this._onNodeAdded.bind(this), this);
|
||||
|
||||
//and for nodes that are removed
|
||||
component.on('nodeRemoved', this._onNodeRemoved.bind(this), this);
|
||||
component.on('nodeWasRemoved', this._onNodeWasRemoved.bind(this), this);
|
||||
|
||||
this.emit('componentAdded', component);
|
||||
};
|
||||
|
||||
GraphModel.prototype.removeComponentWithName = async function (componentName) {
|
||||
if (this.components.hasOwnProperty(componentName) === false) {
|
||||
console.error('GraphModel: Component with name ' + componentName + ' not in graph');
|
||||
return;
|
||||
}
|
||||
|
||||
var component = this.components[componentName];
|
||||
await component.reset();
|
||||
|
||||
component.removeAllListeners();
|
||||
delete this.components[component.name];
|
||||
|
||||
this.emit('componentRemoved', component);
|
||||
};
|
||||
|
||||
GraphModel.prototype.renameComponent = function (componentName, newName) {
|
||||
if (this.components.hasOwnProperty(componentName) === false) {
|
||||
console.error('GraphModel: Component with name ' + componentName + ' not in graph');
|
||||
return;
|
||||
}
|
||||
|
||||
this.getNodesWithType(componentName).forEach(function (nodeModel) {
|
||||
nodeModel.type = newName;
|
||||
});
|
||||
|
||||
var component = this.components[componentName];
|
||||
component.rename(newName);
|
||||
|
||||
delete this.components[componentName];
|
||||
this.components[newName] = component;
|
||||
|
||||
this.emit('componentRenamed', component);
|
||||
};
|
||||
|
||||
GraphModel.prototype._addComponentPorts = function (node) {
|
||||
//check if this node is a known component and add port to the model
|
||||
if (this.components.hasOwnProperty(node.type)) {
|
||||
//a component was created, add component ports to model
|
||||
var component = this.components[node.type];
|
||||
|
||||
const inputPorts = component.getInputPorts();
|
||||
const outputPorts = component.getOutputPorts();
|
||||
|
||||
Object.keys(inputPorts).forEach((portName) => {
|
||||
node.addInputPort(inputPorts[portName]);
|
||||
});
|
||||
|
||||
Object.keys(outputPorts).forEach((portName) => {
|
||||
node.addOutputPort(outputPorts[portName]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
GraphModel.prototype._onNodeAdded = function (node) {
|
||||
this._addComponentPorts(node);
|
||||
|
||||
this.emit('nodeAdded', node);
|
||||
this.emit('nodeAdded.' + node.type, node);
|
||||
};
|
||||
|
||||
GraphModel.prototype._onNodeRemoved = function (node) {
|
||||
this.emit('nodeRemoved', node);
|
||||
this.emit('nodeRemoved.' + node.type, node);
|
||||
};
|
||||
|
||||
GraphModel.prototype._onNodeWasRemoved = function (node) {
|
||||
this.emit('nodeWasRemoved', node);
|
||||
this.emit('nodeWasRemoved.' + node.type, node);
|
||||
};
|
||||
|
||||
GraphModel.prototype.reset = async function () {
|
||||
for (const componentName of Object.keys(this.components)) {
|
||||
await this.removeComponentWithName(componentName);
|
||||
}
|
||||
this.setSettings({});
|
||||
};
|
||||
|
||||
GraphModel.prototype.isEmpty = function () {
|
||||
return Object.keys(this.components).length === 0;
|
||||
};
|
||||
|
||||
GraphModel.prototype.setSettings = function (settings) {
|
||||
this.settings = settings;
|
||||
this.emit('projectSettingsChanged', settings);
|
||||
};
|
||||
|
||||
GraphModel.prototype.getSettings = function () {
|
||||
return this.settings;
|
||||
};
|
||||
|
||||
GraphModel.prototype.setAllMetaData = function (metadata) {
|
||||
for (const p in metadata) {
|
||||
this.setMetaData(p, metadata[p]);
|
||||
}
|
||||
};
|
||||
|
||||
GraphModel.prototype.setMetaData = function (key, data) {
|
||||
//metadata changes can trigger lots of ports to evaluate (e.g. when a database model has been changed)
|
||||
//check if the data actually has been updated before since the editor can send the same data multiple times
|
||||
if (this.metadata[key] && JSON.stringify(this.metadata[key]) === JSON.stringify(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.metadata[key] = data;
|
||||
this.emit('metadataChanged', { key, data });
|
||||
this.emit('metadataChanged.' + key, data);
|
||||
};
|
||||
|
||||
GraphModel.prototype.getMetaData = function (key) {
|
||||
if (key) return this.metadata[key];
|
||||
return this.metadata;
|
||||
};
|
||||
|
||||
GraphModel.prototype.getVariants = function () {
|
||||
return this.variants || [];
|
||||
};
|
||||
|
||||
GraphModel.prototype.getVariant = function (typename, name) {
|
||||
return this.variants.find((v) => v.name === name && v.typename === typename);
|
||||
};
|
||||
|
||||
GraphModel.prototype.updateVariant = function (variant) {
|
||||
const i = this.variants.findIndex((v) => v.name === variant.name && v.typename === variant.typename);
|
||||
if (i !== -1) this.variants.splice(i, 1);
|
||||
this.variants.push(variant);
|
||||
|
||||
this.emit('variantUpdated', variant);
|
||||
};
|
||||
|
||||
GraphModel.prototype.updateVariantParameter = function (
|
||||
variantName,
|
||||
variantTypeName,
|
||||
parameterName,
|
||||
parameterValue,
|
||||
state
|
||||
) {
|
||||
const variant = this.getVariant(variantTypeName, variantName);
|
||||
if (!variant) {
|
||||
console.log("updateVariantParameter: can't find variant", variantName, variantTypeName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
if (parameterValue === undefined) {
|
||||
delete variant.parameters[parameterName];
|
||||
} else {
|
||||
variant.parameters[parameterName] = parameterValue;
|
||||
}
|
||||
} else {
|
||||
if (!variant.stateParameters.hasOwnProperty(state)) {
|
||||
variant.stateParameters[state] = {};
|
||||
}
|
||||
|
||||
if (parameterValue === undefined) {
|
||||
delete variant.stateParameters[state][parameterName];
|
||||
} else {
|
||||
variant.stateParameters[state][parameterName] = parameterValue;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('variantUpdated', variant);
|
||||
};
|
||||
|
||||
GraphModel.prototype.updateVariantDefaultStateTransition = function (variantName, variantTypeName, transition, state) {
|
||||
const variant = this.getVariant(variantTypeName, variantName);
|
||||
if (!variant) return;
|
||||
|
||||
variant.defaultStateTransitions[state] = transition;
|
||||
this.emit('variantUpdated', variant);
|
||||
};
|
||||
|
||||
GraphModel.prototype.updateVariantStateTransition = function (args) {
|
||||
const { variantTypeName, variantName, state, parameterName, curve } = args;
|
||||
|
||||
const variant = this.getVariant(variantTypeName, variantName);
|
||||
if (!variant) return;
|
||||
|
||||
if (!variant.stateTransitions[state]) {
|
||||
variant.stateTransitions[state] = {};
|
||||
}
|
||||
|
||||
variant.stateTransitions[state][parameterName] = curve;
|
||||
};
|
||||
|
||||
GraphModel.prototype.deleteVariant = function (typename, name) {
|
||||
const i = this.variants.findIndex((v) => v.name === name && v.typename === typename);
|
||||
if (i !== -1) this.variants.splice(i, 1);
|
||||
};
|
||||
|
||||
module.exports = GraphModel;
|
||||
203
packages/noodl-runtime/src/models/nodemodel.js
Normal file
203
packages/noodl-runtime/src/models/nodemodel.js
Normal file
@@ -0,0 +1,203 @@
|
||||
"use strict";
|
||||
|
||||
var EventSender = require('../eventsender');
|
||||
|
||||
function NodeModel(id, type) {
|
||||
EventSender.call(this);
|
||||
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
|
||||
this.inputs = [];
|
||||
this.outputs = [];
|
||||
this.children = [];
|
||||
this.parameters = {};
|
||||
this.inputPorts = {};
|
||||
this.outputPorts = {};
|
||||
}
|
||||
|
||||
NodeModel.prototype = Object.create(EventSender.prototype);
|
||||
|
||||
NodeModel.prototype.setParameter = function(name, value, state) {
|
||||
if(state) {
|
||||
if(!this.stateParameters) this.stateParameters = {};
|
||||
if(!this.stateParameters[state]) this.stateParameters[state] = {};
|
||||
|
||||
if(value === undefined) {
|
||||
delete this.stateParameters[state][name];
|
||||
}
|
||||
else {
|
||||
this.stateParameters[state][name] = value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(value === undefined) {
|
||||
delete this.parameters[name];
|
||||
}
|
||||
else {
|
||||
this.parameters[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("parameterUpdated", {name, value, state});
|
||||
};
|
||||
|
||||
NodeModel.prototype.setParameters = function(parameters) {
|
||||
Object.keys(parameters).forEach(name => {
|
||||
this.setParameter(name, parameters[name]);
|
||||
});
|
||||
};
|
||||
|
||||
NodeModel.prototype.setStateParameters = function(parameters) {
|
||||
this.stateParameters = parameters;
|
||||
};
|
||||
|
||||
NodeModel.prototype.setStateTransitions = function(stateTransitions) {
|
||||
this.stateTransitions = stateTransitions;
|
||||
};
|
||||
|
||||
NodeModel.prototype.setStateTransitionParamter = function(parameter, curve, state) {
|
||||
if(!this.stateTransitions) {
|
||||
this.stateTransitions = {};
|
||||
}
|
||||
|
||||
if(curve) {
|
||||
this.stateTransitions[state][parameter] = curve;
|
||||
}
|
||||
else {
|
||||
delete this.stateTransitions[state][parameter];
|
||||
}
|
||||
};
|
||||
|
||||
NodeModel.prototype.setDefaultStateTransition = function(stateTransition, state) {
|
||||
if(!this.defaultStateTransitions) {
|
||||
this.defaultStateTransitions = {};
|
||||
}
|
||||
this.defaultStateTransitions[state] = stateTransition;
|
||||
};
|
||||
|
||||
NodeModel.prototype.addInputPort = function(port) {
|
||||
this.inputPorts[port.name] = port;
|
||||
this.emit("inputPortAdded", port);
|
||||
};
|
||||
|
||||
NodeModel.prototype.getInputPort = function(portName) {
|
||||
return this.inputPorts[portName];
|
||||
};
|
||||
|
||||
NodeModel.prototype.getInputPorts = function() {
|
||||
return this.inputPorts;
|
||||
};
|
||||
|
||||
NodeModel.prototype.removeInputPortWithName = function(portName) {
|
||||
if(this.inputPorts.hasOwnProperty(portName)) {
|
||||
var port = this.inputPorts[portName];
|
||||
delete this.inputPorts[portName];
|
||||
this.emit("inputPortRemoved", port);
|
||||
}
|
||||
};
|
||||
|
||||
NodeModel.prototype.updateInputPortTypes = function(ports) {
|
||||
var changed = false;
|
||||
for(var key in ports) {
|
||||
if(this.inputPorts[key] !== undefined) {
|
||||
this.inputPorts[key].type = ports[key].type;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed && this.emit("inputPortTypesUpdated");
|
||||
}
|
||||
|
||||
NodeModel.prototype.addOutputPort = function(port) {
|
||||
this.outputPorts[port.name] = port;
|
||||
this.emit("outputPortAdded", port);
|
||||
};
|
||||
|
||||
NodeModel.prototype.getOutputPort = function(portName) {
|
||||
return this.outputPorts[portName];
|
||||
};
|
||||
|
||||
NodeModel.prototype.getOutputPorts = function() {
|
||||
return this.outputPorts;
|
||||
};
|
||||
|
||||
NodeModel.prototype.removeOutputPortWithName = function(portName) {
|
||||
if(this.outputPorts.hasOwnProperty(portName)) {
|
||||
var port = this.outputPorts[portName];
|
||||
delete this.outputPorts[portName];
|
||||
this.emit("outputPortRemoved", port);
|
||||
}
|
||||
};
|
||||
|
||||
NodeModel.prototype.updateOutputPortTypes = function(ports) {
|
||||
var changed = false;
|
||||
for(var key in ports) {
|
||||
if(this.outputPorts[key] !== undefined) {
|
||||
this.outputPorts[key].type = ports[key].type;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed && this.emit("outputPortTypesUpdated");
|
||||
}
|
||||
|
||||
NodeModel.prototype.addChild = function(child, index) {
|
||||
child.parent = this;
|
||||
if(index === undefined) {
|
||||
this.children.push(child);
|
||||
}
|
||||
else {
|
||||
this.children.splice(index, 0, child);
|
||||
}
|
||||
this.emit("childAdded", child);
|
||||
};
|
||||
|
||||
NodeModel.prototype.removeChild = function(child) {
|
||||
child.parent = undefined;
|
||||
var index = this.children.indexOf(child);
|
||||
this.children.splice(index, 1);
|
||||
this.emit("childRemoved", child);
|
||||
};
|
||||
|
||||
NodeModel.prototype.reset = function() {
|
||||
this.removeAllListeners();
|
||||
};
|
||||
|
||||
NodeModel.prototype.setVariant = function(variant) {
|
||||
this.variant = variant;
|
||||
this.emit("variantUpdated", variant);
|
||||
};
|
||||
|
||||
NodeModel.createFromExportData = function(nodeData) {
|
||||
var node = new NodeModel(nodeData.id, nodeData.type);
|
||||
nodeData.parameters && node.setParameters(nodeData.parameters);
|
||||
nodeData.stateParameters && node.setStateParameters(nodeData.stateParameters);
|
||||
nodeData.stateTransitions && node.setStateTransitions(nodeData.stateTransitions);
|
||||
|
||||
if(nodeData.defaultStateTransitions) {
|
||||
for(const state in nodeData.defaultStateTransitions) {
|
||||
node.setDefaultStateTransition(nodeData.defaultStateTransitions[state], state);
|
||||
}
|
||||
}
|
||||
|
||||
nodeData.ports && nodeData.ports.forEach(function(port) {
|
||||
|
||||
//some ports are incorrectly named outputs instead of output, patch it here so
|
||||
//the rest of the code doesn't need to care
|
||||
if(port.plug === "outputs") {
|
||||
port.plug = "output";
|
||||
}
|
||||
|
||||
if(port.plug === "input" || port.plug === "input/output") {
|
||||
node.addInputPort(port);
|
||||
}
|
||||
if(port.plug === "output" || port.plug === "input/output") {
|
||||
node.addOutputPort(port);
|
||||
}
|
||||
});
|
||||
|
||||
nodeData.variant && node.setVariant(nodeData.variant);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
module.exports = NodeModel;
|
||||
592
packages/noodl-runtime/src/node.js
Normal file
592
packages/noodl-runtime/src/node.js
Normal file
@@ -0,0 +1,592 @@
|
||||
const OutputProperty = require('./outputproperty');
|
||||
|
||||
/**
|
||||
* Base class for all Nodes
|
||||
* @constructor
|
||||
*/
|
||||
function Node(context, id) {
|
||||
this.id = id;
|
||||
this.context = context;
|
||||
this._dirty = false;
|
||||
|
||||
this._inputs = {};
|
||||
this._inputValues = {};
|
||||
this._outputs = {};
|
||||
|
||||
this._inputConnections = {};
|
||||
this._outputList = [];
|
||||
this._isUpdating = false;
|
||||
this._inputValuesQueue = {};
|
||||
this._afterInputsHaveUpdatedCallbacks = [];
|
||||
|
||||
this._internal = {};
|
||||
this._signalsSentThisUpdate = {};
|
||||
|
||||
this._deleted = false;
|
||||
this._deleteListeners = [];
|
||||
this._isFirstUpdate = true;
|
||||
|
||||
this._valuesFromConnections = {};
|
||||
this.updateOnDirtyFlagging = true;
|
||||
}
|
||||
|
||||
Node.prototype.getInputValue = function (name) {
|
||||
return this._inputValues[name];
|
||||
};
|
||||
|
||||
Node.prototype.registerInput = function (name, input) {
|
||||
if (this.hasInput(name)) {
|
||||
throw new Error('Input property ' + name + ' already registered');
|
||||
}
|
||||
|
||||
this._inputs[name] = input;
|
||||
|
||||
if (input.type && input.type.units) {
|
||||
const defaultUnit = input.type.defaultUnit || input.type.units[0];
|
||||
this._inputValues[name] = {
|
||||
value: input.default,
|
||||
type: defaultUnit
|
||||
};
|
||||
} else if (input.hasOwnProperty('default')) {
|
||||
this._inputValues[name] = input.default;
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.deregisterInput = function (name) {
|
||||
if (this.hasInput(name) === false) {
|
||||
throw new Error('Input property ' + name + " doesn't exist");
|
||||
}
|
||||
delete this._inputs[name];
|
||||
delete this._inputValues[name];
|
||||
};
|
||||
|
||||
Node.prototype.registerInputs = function (inputs) {
|
||||
for (const name in inputs) {
|
||||
this.registerInput(name, inputs[name]);
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.getInput = function (name) {
|
||||
if (this.hasInput(name) === false) {
|
||||
console.log('Node ' + this.name + ': Invalid input property ' + name);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._inputs[name];
|
||||
};
|
||||
|
||||
Node.prototype.hasInput = function (name) {
|
||||
return name in this._inputs;
|
||||
};
|
||||
|
||||
Node.prototype.registerInputIfNeeded = function () {
|
||||
//noop, can be overriden by subclasses
|
||||
};
|
||||
|
||||
Node.prototype.setInputValue = function (name, value) {
|
||||
const input = this.getInput(name);
|
||||
if (!input) {
|
||||
console.log("node doesn't have input", name);
|
||||
return;
|
||||
}
|
||||
|
||||
//inputs with units always expect objects in the shape of {value, unit, ...}
|
||||
//these inputs might sometimes get raw numbers without units, and in those cases
|
||||
//Noodl should just update the value and not the other parameters
|
||||
const currentInputValue = this._inputValues[name];
|
||||
|
||||
if (isNaN(value) === false && currentInputValue && currentInputValue.unit) {
|
||||
//update the value, and keep the other parameters
|
||||
const newValue = Object.assign({}, currentInputValue); //copy it, so we don't modify the original object (e.g. it might come from a variant)
|
||||
newValue.value = value;
|
||||
value = newValue;
|
||||
}
|
||||
|
||||
//Save the current input value. Save it before resolving color styles so delta updates on color styles work correctly
|
||||
this._inputValues[name] = value;
|
||||
|
||||
if (input.type === 'color' && this.context && this.context.styles) {
|
||||
value = this.context.styles.resolveColor(value);
|
||||
} else if (input.type === 'array' && typeof value === 'string') {
|
||||
try {
|
||||
value = eval(value);
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'invalid-array-' + name);
|
||||
} catch (e) {
|
||||
value = [];
|
||||
console.log(e);
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'invalid-array-' + name,
|
||||
{
|
||||
showGlobally: true,
|
||||
message: 'Invalid array<br>' + e.toString()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.set.call(this, value);
|
||||
};
|
||||
|
||||
Node.prototype.hasOutput = function (name) {
|
||||
return name in this._outputs;
|
||||
};
|
||||
|
||||
Node.prototype.registerOutput = function (name, output) {
|
||||
if (this.hasOutput(name)) {
|
||||
throw new Error('Output property ' + name + ' already registered');
|
||||
}
|
||||
|
||||
const newOutput = new OutputProperty({
|
||||
owner: this,
|
||||
getter: output.get || output.getter,
|
||||
name: name,
|
||||
onFirstConnectionAdded: output.onFirstConnectionAdded,
|
||||
onLastConnectionRemoved: output.onLastConnectionRemoved
|
||||
});
|
||||
|
||||
this._outputs[name] = newOutput;
|
||||
this._outputList.push(newOutput);
|
||||
};
|
||||
|
||||
Node.prototype.deregisterOutput = function (name) {
|
||||
if (this.hasOutput(name) === false) {
|
||||
throw new Error('Output property ' + name + " isn't registered");
|
||||
}
|
||||
|
||||
const output = this._outputs[name];
|
||||
|
||||
if (output.hasConnections()) {
|
||||
throw new Error('Output property ' + name + " has connections and can't be removed");
|
||||
}
|
||||
|
||||
delete this._outputs[name];
|
||||
var index = this._outputList.indexOf(output);
|
||||
this._outputList.splice(index, 1);
|
||||
};
|
||||
|
||||
Node.prototype.registerOutputs = function (outputs) {
|
||||
for (var name in outputs) {
|
||||
this.registerOutput(name, outputs[name]);
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.registerOutputIfNeeded = function () {
|
||||
//noop, can be overriden by subclasses
|
||||
};
|
||||
|
||||
Node.prototype.getOutput = function (name) {
|
||||
if (this.hasOutput(name) === false) {
|
||||
throw new Error('Node ' + this.name + " doesn't have a port named " + name);
|
||||
}
|
||||
return this._outputs[name];
|
||||
};
|
||||
|
||||
Node.prototype.connectInput = function (inputName, sourceNode, sourcePortName) {
|
||||
if (this.hasInput(inputName) === false) {
|
||||
throw new Error(
|
||||
"Invalid connection, input doesn't exist. Trying to connect from " +
|
||||
sourceNode.name +
|
||||
' output ' +
|
||||
sourcePortName +
|
||||
' to ' +
|
||||
this.name +
|
||||
' input ' +
|
||||
inputName
|
||||
);
|
||||
}
|
||||
|
||||
var sourcePort = sourceNode.getOutput(sourcePortName);
|
||||
sourcePort.registerConnection(this, inputName);
|
||||
|
||||
if (!this._inputConnections[inputName]) {
|
||||
this._inputConnections[inputName] = [];
|
||||
}
|
||||
|
||||
this._inputConnections[inputName].push(sourcePort);
|
||||
|
||||
if (sourceNode._signalsSentThisUpdate[sourcePortName]) {
|
||||
this._setValueFromConnection(inputName, true);
|
||||
this._setValueFromConnection(inputName, false);
|
||||
} else {
|
||||
var outputValue = sourcePort.value;
|
||||
if (outputValue !== undefined) {
|
||||
this._setValueFromConnection(inputName, outputValue);
|
||||
|
||||
if (this.context) {
|
||||
// Send value to editor for connection debugging.
|
||||
// Conceptually the value has already been sent,
|
||||
// but the editor needs to be notified after a connection is created
|
||||
this.context.connectionSentValue(sourcePort, sourcePort.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.flagDirty();
|
||||
};
|
||||
|
||||
Node.prototype.removeInputConnection = function (inputName, sourceNodeId, sourcePortName) {
|
||||
if (!this._inputConnections[inputName]) {
|
||||
throw new Error("Node removeInputConnection: Input doesn't exist");
|
||||
}
|
||||
|
||||
const inputsToPort = this._inputConnections[inputName];
|
||||
|
||||
for (let i = 0; i < inputsToPort.length; i++) {
|
||||
const sourcePort = inputsToPort[i];
|
||||
if (sourcePort.owner.id === sourceNodeId && sourcePort.name === sourcePortName) {
|
||||
inputsToPort.splice(i, 1);
|
||||
|
||||
//remove the output from the source node
|
||||
const output = sourcePort.owner.getOutput(sourcePortName);
|
||||
output.deregisterConnection(this, inputName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (inputsToPort.length === 0) {
|
||||
//no inputs left, remove the bookkeeping that traces values sent to this node as inputs
|
||||
delete this._valuesFromConnections[inputName];
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.isInputConnected = function (inputName) {
|
||||
if (!this._inputConnections.hasOwnProperty(inputName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//We have connections, but they might be from non-connected component inputs.
|
||||
// If they are from a component input then check the input on the component instance.
|
||||
return this._inputConnections[inputName].some((c) => {
|
||||
//if this is not a component input, then we have a proper connection
|
||||
if (c.owner.name !== 'Component Inputs') return true;
|
||||
|
||||
//the name of the output from the component input, is the same as the component instance input
|
||||
const component = c.owner.nodeScope.componentOwner;
|
||||
return component.isInputConnected(c.name);
|
||||
});
|
||||
};
|
||||
|
||||
Node.prototype.update = function () {
|
||||
if (this._isUpdating || this._dirty === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._updatedAtIteration !== this.context.updateIteration) {
|
||||
this._updatedAtIteration = this.context.updateIteration;
|
||||
this._updateIteration = 0;
|
||||
if (this._cyclicLoop) this._cyclicLoop = false;
|
||||
}
|
||||
|
||||
this._isUpdating = true;
|
||||
const maxUpdateIterations = 100;
|
||||
|
||||
try {
|
||||
while (this._dirty && !this._cyclicLoop) {
|
||||
this._updateDependencies();
|
||||
|
||||
//all inputs are now updated, flag as not dirty
|
||||
this._dirty = false;
|
||||
|
||||
const inputNames = Object.keys(this._inputValuesQueue);
|
||||
|
||||
let hasMoreInputs = true;
|
||||
|
||||
while (hasMoreInputs && !this._cyclicLoop) {
|
||||
hasMoreInputs = false;
|
||||
|
||||
for (let i = 0; i < inputNames.length; i++) {
|
||||
const inputName = inputNames[i];
|
||||
const queue = this._inputValuesQueue[inputName];
|
||||
if (queue.length > 0) {
|
||||
this.setInputValue(inputName, queue.shift());
|
||||
if (queue.length > 0) {
|
||||
hasMoreInputs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const afterInputCallbacks = this._afterInputsHaveUpdatedCallbacks;
|
||||
this._afterInputsHaveUpdatedCallbacks = [];
|
||||
for (let i = 0; i < afterInputCallbacks.length; i++) {
|
||||
afterInputCallbacks[i].call(this);
|
||||
}
|
||||
}
|
||||
this._updateIteration++;
|
||||
|
||||
if (this._updateIteration >= maxUpdateIterations) {
|
||||
this._cyclicLoop = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this._isUpdating = false;
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (this._cyclicLoop) {
|
||||
//flag the node as dirty again to let it contiune next frame so we don't just stop it
|
||||
//This will allow the browser a chance to render and run other code
|
||||
this.context.scheduleNextFrame(() => {
|
||||
this.context.nodeIsDirty(this);
|
||||
});
|
||||
|
||||
if (this.context.editorConnection && !this._cyclicWarningSent && this.context.isWarningTypeEnabled('cyclicLoops')) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'cyclic-loop', {
|
||||
showGlobally: true,
|
||||
message: 'Cyclic loop detected'
|
||||
});
|
||||
this._cyclicWarningSent = true;
|
||||
console.log('cycle detected', {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
component: this.nodeScope.componentOwner.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._isFirstUpdate = false;
|
||||
this._isUpdating = false;
|
||||
};
|
||||
|
||||
Node.prototype._updateDependencies = function () {
|
||||
for (var inputName in this._inputConnections) {
|
||||
var connectedPorts = this._inputConnections[inputName];
|
||||
for (var i = 0; i < connectedPorts.length; ++i) {
|
||||
connectedPorts[i].owner.update();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.flagDirty = function () {
|
||||
if (this._dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
|
||||
//a hack to not update nodes as a component is being created.
|
||||
//Nodes should update once all connections are in place, so inputs that rely on connections, e.g. "Run" on a Function node, have the correct context before running.
|
||||
//This flag is being updated externally by the NodeScope and _performDirtyUpdate will be called when the component setup is done
|
||||
if (this.updateOnDirtyFlagging) {
|
||||
this._performDirtyUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype._performDirtyUpdate = function () {
|
||||
this.context && this.context.nodeIsDirty(this);
|
||||
|
||||
for (var i = 0; i < this._outputList.length; ++i) {
|
||||
this._outputList[i].flagDependeesDirty();
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.sendValue = function (name, value) {
|
||||
if (this.hasOutput(name) === false) {
|
||||
console.log('Error: Node', this.name, "doesn't have a output named", name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = this.getOutput(name);
|
||||
output.sendValue(value);
|
||||
|
||||
if (this.context) {
|
||||
this.context.connectionSentValue(output, value);
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.flagOutputDirty = function (name) {
|
||||
const output = this.getOutput(name);
|
||||
this.sendValue(name, output.value);
|
||||
};
|
||||
|
||||
Node.prototype.flagAllOutputsDirty = function () {
|
||||
for (const output of this._outputList) {
|
||||
this.sendValue(output.name, output.value);
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.sendSignalOnOutput = function (outputName) {
|
||||
if (this.hasOutput(outputName) === false) {
|
||||
console.log('Error: Node', this.name, "doesn't have a output named", outputName);
|
||||
return;
|
||||
}
|
||||
|
||||
const output = this.getOutput(outputName);
|
||||
output.sendValue(true);
|
||||
output.sendValue(false);
|
||||
|
||||
this._signalsSentThisUpdate[outputName] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
this._signalsSentThisUpdate[outputName] = false;
|
||||
});
|
||||
|
||||
if (this.context) {
|
||||
this.context.connectionSentSignal(output);
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype._setValueFromConnection = function (inputName, value) {
|
||||
this._valuesFromConnections[inputName] = value;
|
||||
this.queueInput(inputName, value);
|
||||
};
|
||||
|
||||
Node.prototype._hasInputBeenSetFromAConnection = function (inputName) {
|
||||
return this._valuesFromConnections.hasOwnProperty(inputName);
|
||||
};
|
||||
|
||||
Node.prototype.queueInput = function (inputName, value) {
|
||||
if (!this._inputValuesQueue[inputName]) {
|
||||
this._inputValuesQueue[inputName] = [];
|
||||
}
|
||||
|
||||
//when values are queued during the very first update, make the last value overwrite previous ones
|
||||
//so a chain with multiple nodes with values that connect to each other all
|
||||
//consolidate to a single value, instead of piling up in the queue
|
||||
if (this._isFirstUpdate) {
|
||||
//signals need two values, so make sure we don't suppress the 'false' that comes directly
|
||||
//after a 'true'
|
||||
const queueValue = this._inputValuesQueue[inputName][0];
|
||||
const isSignal = queueValue === true; // && value === true;
|
||||
if (!isSignal) {
|
||||
//default units are set as an object {value, unit}
|
||||
//subsequent inputs can be unitless. and will will then overwrite those
|
||||
//and the node will get a value without ever getting a unit.
|
||||
//To make sure that doesn't happen, look at the value being overwritten
|
||||
//and use the unit from that before overwriting
|
||||
if (queueValue instanceof Object && queueValue.unit && value instanceof Object === false) {
|
||||
value = {
|
||||
value,
|
||||
unit: queueValue.unit
|
||||
};
|
||||
}
|
||||
|
||||
this._inputValuesQueue[inputName].length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this._inputValuesQueue[inputName].push(value);
|
||||
this.flagDirty();
|
||||
};
|
||||
|
||||
Node.prototype.scheduleAfterInputsHaveUpdated = function (callback) {
|
||||
this._afterInputsHaveUpdatedCallbacks.push(callback);
|
||||
this.flagDirty();
|
||||
};
|
||||
|
||||
Node.prototype.setNodeModel = function (nodeModel) {
|
||||
this.model = nodeModel;
|
||||
nodeModel.on('parameterUpdated', this._onNodeModelParameterUpdated, this);
|
||||
nodeModel.on('variantUpdated', this._onNodeModelVariantUpdated, this);
|
||||
|
||||
nodeModel.on(
|
||||
'inputPortRemoved',
|
||||
(port) => {
|
||||
if (this.hasInput(port.name)) {
|
||||
this.deregisterInput(port.name);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
nodeModel.on(
|
||||
'outputPortRemoved',
|
||||
(port) => {
|
||||
if (this.hasOutput(port.name)) {
|
||||
this.deregisterOutput(port.name);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
};
|
||||
|
||||
Node.prototype.addDeleteListener = function (listener) {
|
||||
this._deleteListeners.push(listener);
|
||||
};
|
||||
|
||||
Node.prototype._onNodeDeleted = function () {
|
||||
if (this.model) {
|
||||
this.model.removeListenersWithRef(this);
|
||||
this.model = undefined;
|
||||
}
|
||||
|
||||
this._deleted = true;
|
||||
|
||||
for (const deleteListener of this._deleteListeners) {
|
||||
deleteListener.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype._onNodeModelParameterUpdated = function (event) {
|
||||
this.registerInputIfNeeded(event.name);
|
||||
|
||||
if (event.value !== undefined) {
|
||||
if (event.state) {
|
||||
//this parameter is only used in a certain visual state
|
||||
//make sure we are in that state before setting it
|
||||
|
||||
if (!this._getVisualStates) {
|
||||
console.log('Node has nos visual states, but got a parameter for state', event.state);
|
||||
return;
|
||||
}
|
||||
|
||||
const states = this._getVisualStates();
|
||||
if (states.indexOf(event.state) !== -1) {
|
||||
this.queueInput(event.name, event.value);
|
||||
}
|
||||
} else {
|
||||
this.queueInput(event.name, event.value);
|
||||
}
|
||||
} else {
|
||||
//parameter is undefined, that means it has been removed and we should reset to default
|
||||
let defaultValue;
|
||||
|
||||
const variant = this.variant;
|
||||
|
||||
if (event.state) {
|
||||
//local value has been reset, check the variant first
|
||||
if (
|
||||
variant &&
|
||||
variant.stateParameters.hasOwnProperty(event.state) &&
|
||||
variant.stateParameters[event.state].hasOwnProperty(event.name)
|
||||
) {
|
||||
defaultValue = variant.stateParameters[event.state][event.name];
|
||||
}
|
||||
//and if variant has no value in that state, check for local values in the neutral state
|
||||
else if (this.model.parameters.hasOwnProperty(event.name)) {
|
||||
defaultValue = this.model.parameters[event.name];
|
||||
}
|
||||
//and then look in the variant neutral values
|
||||
else if (variant && variant.parameters.hasOwnProperty(event.name)) {
|
||||
defaultValue = variant.parameters[event.name];
|
||||
}
|
||||
} else if (variant && variant.parameters.hasOwnProperty(event.name)) {
|
||||
defaultValue = variant.parameters[event.name];
|
||||
}
|
||||
|
||||
if (defaultValue === undefined) {
|
||||
//get the default value for the port
|
||||
defaultValue = this.context.getDefaultValueForInput(this.model.type, event.name);
|
||||
|
||||
//when a paramter that's used by a text style is reset, Noodl will modify the original dom node, outside of React
|
||||
//React will then re-render, and should apply the values from the text style, but won't see any delta in the virtual dom,
|
||||
//even though there is a diff to the real dom.
|
||||
//to fix that, we just force React to re-render the entire node
|
||||
this._resetReactVirtualDOM && this._resetReactVirtualDOM();
|
||||
}
|
||||
|
||||
this.queueInput(event.name, defaultValue);
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype._onNodeModelVariantUpdated = function (variant) {
|
||||
this.setVariant(variant);
|
||||
};
|
||||
|
||||
module.exports = Node;
|
||||
541
packages/noodl-runtime/src/nodecontext.js
Normal file
541
packages/noodl-runtime/src/nodecontext.js
Normal file
@@ -0,0 +1,541 @@
|
||||
'use strict';
|
||||
|
||||
var EventEmitter = require('./events');
|
||||
var NodeRegister = require('./noderegister');
|
||||
var TimerScheduler = require('./timerscheduler');
|
||||
const Variants = require('./variants');
|
||||
|
||||
function NodeContext(args) {
|
||||
args = args || {};
|
||||
args.runningInEditor = args.hasOwnProperty('runningInEditor') ? args.runningInEditor : false;
|
||||
|
||||
this._dirtyNodes = [];
|
||||
this.callbacksAfterUpdate = [];
|
||||
|
||||
this.graphModel = args.graphModel;
|
||||
|
||||
this.platform = args.platform;
|
||||
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.eventEmitter.setMaxListeners(1000000);
|
||||
|
||||
this.eventSenderEmitter = new EventEmitter(); //used by event senders and receivers
|
||||
this.eventSenderEmitter.setMaxListeners(1000000);
|
||||
|
||||
this.globalValues = {};
|
||||
this.globalsEventEmitter = new EventEmitter();
|
||||
this.globalsEventEmitter.setMaxListeners(1000000);
|
||||
|
||||
this.runningInEditor = args.runningInEditor;
|
||||
this.currentFrameTime = 0;
|
||||
this.frameNumber = 0;
|
||||
this.updateIteration = 0;
|
||||
|
||||
this.nodeRegister = new NodeRegister(this);
|
||||
this.timerScheduler = new TimerScheduler(this.scheduleUpdate.bind(this));
|
||||
|
||||
this.componentModels = {};
|
||||
this.debugInspectorsEnabled = false;
|
||||
this.connectionsToPulse = {};
|
||||
this.connectionsToPulseChanged = false;
|
||||
|
||||
this.debugInspectors = {};
|
||||
|
||||
this.connectionPulsingCallbackScheduled = false;
|
||||
|
||||
this.editorConnection = args.editorConnection;
|
||||
|
||||
this.rootComponent = undefined;
|
||||
|
||||
this._outputHistory = {};
|
||||
this._signalHistory = {};
|
||||
|
||||
this.warningTypes = {}; //default is to send all warning types
|
||||
|
||||
this.bundleFetchesInFlight = new Map();
|
||||
|
||||
this.variants = new Variants({
|
||||
graphModel: this.graphModel,
|
||||
getNodeScope: () => (this.rootComponent ? this.rootComponent.nodeScope : null)
|
||||
});
|
||||
|
||||
if (this.editorConnection) {
|
||||
this.editorConnection.on('debugInspectorsUpdated', (inspectors) => {
|
||||
this.onDebugInspectorsUpdated(inspectors);
|
||||
});
|
||||
|
||||
this.editorConnection.on('getConnectionValue', ({ clientId, connectionId }) => {
|
||||
if (this.editorConnection.clientId !== clientId) return;
|
||||
const connection = this._outputHistory[connectionId];
|
||||
this.editorConnection.sendConnectionValue(connectionId, connection ? connection.value : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NodeContext.prototype.setRootComponent = function (rootComponent) {
|
||||
this.rootComponent = rootComponent;
|
||||
};
|
||||
|
||||
NodeContext.prototype.getCurrentTime = function () {
|
||||
return this.platform.getCurrentTime();
|
||||
};
|
||||
|
||||
NodeContext.prototype.onDebugInspectorsUpdated = function (inspectors) {
|
||||
if (!this.debugInspectorsEnabled) return;
|
||||
|
||||
inspectors = inspectors.map((inspector) => {
|
||||
if (inspector.type === 'connection') {
|
||||
const connection = inspector.connection;
|
||||
inspector.id = connection.fromId + connection.fromProperty;
|
||||
} else if (inspector.type === 'node') {
|
||||
inspector.id = inspector.nodeId;
|
||||
}
|
||||
return inspector;
|
||||
});
|
||||
|
||||
this.debugInspectors = {};
|
||||
inspectors.forEach((inspector) => (this.debugInspectors[inspector.id] = inspector));
|
||||
|
||||
this.sendDebugInspectorValues();
|
||||
};
|
||||
|
||||
NodeContext.prototype.updateDirtyNodes = function () {
|
||||
var i, len;
|
||||
|
||||
var loop = true,
|
||||
iterations = 0;
|
||||
|
||||
this.updateIteration++;
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
while (loop && iterations < 10) {
|
||||
var dirtyNodes = this._dirtyNodes;
|
||||
this._dirtyNodes = [];
|
||||
for (i = 0, len = dirtyNodes.length; i < len; ++i) {
|
||||
try {
|
||||
if (!dirtyNodes[i]._deleted) {
|
||||
dirtyNodes[i].update();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e, e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
//make a new reference and reset array in case new callbacks are scheduled
|
||||
//by the current callbacks
|
||||
var callbacks = this.callbacksAfterUpdate;
|
||||
this.callbacksAfterUpdate = [];
|
||||
for (i = 0, len = callbacks.length; i < len; i++) {
|
||||
try {
|
||||
callbacks[i]();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
loop = this.callbacksAfterUpdate.length > 0 || this._dirtyNodes.length > 0;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
this.isUpdating = false;
|
||||
};
|
||||
|
||||
NodeContext.prototype.update = function () {
|
||||
this.frameNumber++;
|
||||
|
||||
this.updateDirtyNodes();
|
||||
|
||||
if (this.timerScheduler.hasPendingTimers()) {
|
||||
this.scheduleUpdate();
|
||||
this.timerScheduler.runTimers(this.currentFrameTime);
|
||||
}
|
||||
|
||||
if (this.debugInspectorsEnabled) {
|
||||
this.sendDebugInspectorValues();
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype.reset = function () {
|
||||
//removes listeners like device orientation, websockets and more
|
||||
this.eventEmitter.emit('applicationDataReloaded');
|
||||
|
||||
var eventEmitter = this.eventEmitter;
|
||||
['frameStart', 'frameEnd'].forEach(function (name) {
|
||||
eventEmitter.removeAllListeners(name);
|
||||
});
|
||||
|
||||
this.globalValues = {};
|
||||
this._dirtyNodes.length = 0;
|
||||
this.callbacksAfterUpdate.length = 0;
|
||||
|
||||
this.timerScheduler.runningTimers = [];
|
||||
this.timerScheduler.newTimers = [];
|
||||
this.rootComponent = undefined;
|
||||
|
||||
this.clearDebugInspectors();
|
||||
};
|
||||
|
||||
NodeContext.prototype.nodeIsDirty = function (node) {
|
||||
this._dirtyNodes.push(node);
|
||||
this.scheduleUpdate();
|
||||
};
|
||||
|
||||
NodeContext.prototype.scheduleUpdate = function () {
|
||||
this.eventEmitter.emit('scheduleUpdate');
|
||||
};
|
||||
|
||||
NodeContext.prototype.scheduleAfterUpdate = function (func) {
|
||||
this.callbacksAfterUpdate.push(func);
|
||||
this.scheduleUpdate();
|
||||
};
|
||||
|
||||
NodeContext.prototype.scheduleNextFrame = function (func) {
|
||||
this.eventEmitter.once('frameStart', func);
|
||||
this.scheduleUpdate();
|
||||
};
|
||||
|
||||
NodeContext.prototype.setGlobalValue = function (name, value) {
|
||||
this.globalValues[name] = value;
|
||||
this.globalsEventEmitter.emit(name);
|
||||
};
|
||||
|
||||
NodeContext.prototype.getGlobalValue = function (name) {
|
||||
return this.globalValues[name];
|
||||
};
|
||||
|
||||
NodeContext.prototype.registerComponentModel = function (componentModel) {
|
||||
if (this.componentModels.hasOwnProperty(componentModel.name)) {
|
||||
throw new Error('Duplicate component name ' + componentModel.name);
|
||||
}
|
||||
this.componentModels[componentModel.name] = componentModel;
|
||||
|
||||
var self = this;
|
||||
componentModel.on(
|
||||
'renamed',
|
||||
function (event) {
|
||||
delete self.componentModels[event.oldName];
|
||||
self.componentModels[event.newName] = componentModel;
|
||||
},
|
||||
this
|
||||
);
|
||||
};
|
||||
|
||||
NodeContext.prototype.deregisterComponentModel = function (componentModel) {
|
||||
if (this.componentModels.hasOwnProperty(componentModel.name)) {
|
||||
this.componentModels[componentModel.name].removeListenersWithRef(this);
|
||||
delete this.componentModels[componentModel.name];
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype.fetchComponentBundle = async function (name) {
|
||||
const fetchBundle = async (name) => {
|
||||
let baseUrl = Noodl.Env["BaseUrl"] || '/';
|
||||
let bundleUrl = `${baseUrl}noodl_bundles/${name}.json`;
|
||||
|
||||
const response = await fetch(bundleUrl);
|
||||
if (response.status === 404) {
|
||||
throw new Error('Component not found ' + name);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
for (const component of data) {
|
||||
if (this.graphModel.hasComponentWithName(component.name) === false) {
|
||||
await this.graphModel.importComponentFromEditorData(component);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (this.bundleFetchesInFlight.has(name)) {
|
||||
await this.bundleFetchesInFlight.get(name);
|
||||
} else {
|
||||
const promise = fetchBundle(name);
|
||||
this.bundleFetchesInFlight.set(name, promise);
|
||||
await promise;
|
||||
//the promise is kept in bundleFetchesInFlight to mark what bundles have been downloaded
|
||||
//so eventual future requests will just await the resolved promise and resolve immediately
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype.getComponentModel = async function (name) {
|
||||
if (!name) {
|
||||
throw new Error('Component instance must have a name');
|
||||
}
|
||||
|
||||
if (this.componentModels.hasOwnProperty(name) === false) {
|
||||
const bundleName = this.graphModel.getBundleContainingComponent(name);
|
||||
if (!bundleName) {
|
||||
throw new Error("Can't find component model for " + name);
|
||||
}
|
||||
|
||||
//start fetching dependencies in the background
|
||||
for (const bundleDep of this.graphModel.getBundleDependencies(bundleName)) {
|
||||
this.fetchComponentBundle(bundleDep);
|
||||
}
|
||||
|
||||
//and wait for the bundle that has the component we need
|
||||
await this.fetchComponentBundle(bundleName);
|
||||
}
|
||||
|
||||
return this.componentModels[name];
|
||||
};
|
||||
|
||||
NodeContext.prototype.hasComponentModelWithName = function (name) {
|
||||
return this.componentModels.hasOwnProperty(name);
|
||||
};
|
||||
|
||||
NodeContext.prototype.createComponentInstanceNode = async function (componentName, id, nodeScope, extraProps) {
|
||||
var ComponentInstanceNode = require('./nodes/componentinstance');
|
||||
var node = new ComponentInstanceNode(this, id, nodeScope);
|
||||
node.name = componentName;
|
||||
|
||||
for (const prop in extraProps) {
|
||||
node[prop] = extraProps[prop];
|
||||
}
|
||||
|
||||
const componentModel = await this.getComponentModel(componentName);
|
||||
await node.setComponentModel(componentModel);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
NodeContext.prototype._formatConnectionValue = function (value) {
|
||||
if (typeof value === 'object' && value && value.constructor && value.constructor.name === 'Node') {
|
||||
value = '<Node> ' + value.name;
|
||||
} else if (typeof value === 'object' && typeof window !== 'undefined' && value instanceof HTMLElement) {
|
||||
value = `DOM Node <${value.tagName}>`;
|
||||
} else if (typeof value === 'string' && !value.startsWith('[Signal]')) {
|
||||
return '"' + value + '"';
|
||||
} else if (Number.isNaN(value)) {
|
||||
return 'NaN';
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
NodeContext.prototype.connectionSentValue = function (output, value) {
|
||||
if (!this.editorConnection || !this.editorConnection.isConnected() || !this.debugInspectorsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = this.getCurrentTime();
|
||||
|
||||
this._outputHistory[output.id] = {
|
||||
value,
|
||||
timestamp
|
||||
};
|
||||
|
||||
if (this.connectionsToPulse.hasOwnProperty(output.id)) {
|
||||
this.connectionsToPulse[output.id].timestamp = timestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = [];
|
||||
|
||||
output.connections.forEach((connection) => {
|
||||
connections.push(output.owner.id + output.name + connection.node.id + connection.inputPortName);
|
||||
});
|
||||
|
||||
this.connectionsToPulse[output.id] = {
|
||||
timestamp,
|
||||
connections: connections
|
||||
};
|
||||
|
||||
this.connectionsToPulseChanged = true;
|
||||
|
||||
if (this.connectionPulsingCallbackScheduled === false) {
|
||||
this.connectionPulsingCallbackScheduled = true;
|
||||
setTimeout(this.clearOldConnectionPulsing.bind(this), 100);
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype.connectionSentSignal = function (output) {
|
||||
const id = output.id;
|
||||
if (!this._signalHistory.hasOwnProperty(id)) {
|
||||
this._signalHistory[id] = {
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
|
||||
this._signalHistory[id].count++;
|
||||
|
||||
this.connectionSentValue(output, '[Signal] Trigger count ' + this._signalHistory[id].count);
|
||||
};
|
||||
|
||||
NodeContext.prototype.clearDebugInspectors = function () {
|
||||
this.debugInspectors = {};
|
||||
this.connectionsToPulse = {};
|
||||
|
||||
this.editorConnection.sendPulsingConnections(this.connectionsToPulse);
|
||||
};
|
||||
|
||||
NodeContext.prototype.clearOldConnectionPulsing = function () {
|
||||
this.connectionPulsingCallbackScheduled = false;
|
||||
|
||||
var now = this.getCurrentTime();
|
||||
var self = this;
|
||||
|
||||
var connectionIds = Object.keys(this.connectionsToPulse);
|
||||
connectionIds.forEach(function (id) {
|
||||
var con = self.connectionsToPulse[id];
|
||||
if (now - con.timestamp > 100) {
|
||||
self.connectionsToPulseChanged = true;
|
||||
delete self.connectionsToPulse[id];
|
||||
}
|
||||
});
|
||||
|
||||
if (this.connectionsToPulseChanged) {
|
||||
this.connectionsToPulseChanged = false;
|
||||
this.editorConnection.sendPulsingConnections(this.connectionsToPulse);
|
||||
}
|
||||
|
||||
if (Object.keys(this.connectionsToPulse).length > 0) {
|
||||
this.connectionPulsingCallbackScheduled = true;
|
||||
setTimeout(this.clearOldConnectionPulsing.bind(this), 500);
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype._getDebugInspectorValueForNode = function (id) {
|
||||
if (!this.rootComponent) return;
|
||||
const nodes = this.rootComponent.nodeScope.getNodesWithIdRecursive(id);
|
||||
const node = nodes[nodes.length - 1];
|
||||
|
||||
if (node && node.getInspectInfo) {
|
||||
const info = node.getInspectInfo();
|
||||
if (info !== undefined) {
|
||||
return { type: 'node', id, value: info };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype.sendDebugInspectorValues = function () {
|
||||
const valuesToSend = [];
|
||||
|
||||
for (const id in this.debugInspectors) {
|
||||
const inspector = this.debugInspectors[id];
|
||||
|
||||
if (inspector.type === 'connection' && this._outputHistory.hasOwnProperty(id)) {
|
||||
const value = this._outputHistory[id].value;
|
||||
|
||||
valuesToSend.push({
|
||||
type: 'connection',
|
||||
id,
|
||||
value: this._formatConnectionValue(value)
|
||||
});
|
||||
} else if (inspector.type === 'node') {
|
||||
const inspectorValue = this._getDebugInspectorValueForNode(id);
|
||||
inspectorValue && valuesToSend.push(inspectorValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (valuesToSend.length > 0) {
|
||||
this.editorConnection.sendDebugInspectorValues(valuesToSend);
|
||||
}
|
||||
|
||||
if (this.connectionsToPulseChanged) {
|
||||
this.connectionsToPulseChanged = false;
|
||||
this.editorConnection.sendPulsingConnections(this.connectionsToPulse);
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype.setDebugInspectorsEnabled = function (enabled) {
|
||||
this.debugInspectorsEnabled = enabled;
|
||||
this.editorConnection.debugInspectorsEnabled = enabled;
|
||||
if (enabled) {
|
||||
this.sendDebugInspectorValues();
|
||||
}
|
||||
};
|
||||
|
||||
NodeContext.prototype.sendGlobalEventFromEventSender = function (channelName, inputValues) {
|
||||
this.eventSenderEmitter.emit(channelName, inputValues);
|
||||
};
|
||||
|
||||
NodeContext.prototype.setPopupCallbacks = function ({ onShow, onClose }) {
|
||||
this.onShowPopup = onShow;
|
||||
this.onClosePopup = onClose;
|
||||
};
|
||||
|
||||
NodeContext.prototype.showPopup = async function (popupComponent, params, args) {
|
||||
if (!this.onShowPopup) return;
|
||||
|
||||
const nodeScope = this.rootComponent.nodeScope;
|
||||
|
||||
const popupNode = await nodeScope.createNode(popupComponent);
|
||||
for (const inputKey in params) {
|
||||
popupNode.setInputValue(inputKey, params[inputKey]);
|
||||
}
|
||||
|
||||
popupNode.popupParent = args?.senderNode || null;
|
||||
|
||||
// Create container group
|
||||
const group = nodeScope.createPrimitiveNode('Group');
|
||||
group.setInputValue('flexDirection', 'node');
|
||||
group.setInputValue('cssClassName', 'noodl-popup');
|
||||
|
||||
const bodyScroll = this.graphModel.getSettings().bodyScroll;
|
||||
|
||||
//if the body can scroll the position of the popup needs to be fixed.
|
||||
group.setInputValue('position', bodyScroll ? 'fixed' : 'absolute');
|
||||
|
||||
var closePopupNodes = popupNode.nodeScope.getNodesWithType('NavigationClosePopup');
|
||||
if (closePopupNodes && closePopupNodes.length > 0) {
|
||||
for (var j = 0; j < closePopupNodes.length; j++) {
|
||||
closePopupNodes[j]._setCloseCallback((action, results) => {
|
||||
//close next frame so all nodes have a chance to update before being deleted
|
||||
this.scheduleNextFrame(() => {
|
||||
//avoid double callbacks
|
||||
if (!nodeScope.hasNodeWithId(group.id)) return;
|
||||
|
||||
this.onClosePopup(group);
|
||||
nodeScope.deleteNode(group);
|
||||
args && args.onClosePopup && args.onClosePopup(action, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.onShowPopup(group);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
//hack to make the react components have the right props
|
||||
//TODO: figure out why this requestAnimationFrame is necessary
|
||||
group.addChild(popupNode);
|
||||
});
|
||||
};
|
||||
|
||||
NodeContext.prototype.setWarningTypes = function (warningTypes) {
|
||||
Object.assign(this.warningTypes, warningTypes);
|
||||
};
|
||||
|
||||
NodeContext.prototype.isWarningTypeEnabled = function (warning) {
|
||||
if (!this.warningTypes.hasOwnProperty(warning)) {
|
||||
//if a level isn't set, default to true
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.warningTypes[warning] ? true : false;
|
||||
};
|
||||
|
||||
NodeContext.prototype.getDefaultValueForInput = function (nodeType, inputName) {
|
||||
if (this.nodeRegister.hasNode(nodeType) === false) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nodeMetadata = this.nodeRegister.getNodeMetadata(nodeType);
|
||||
const inputMetadata = nodeMetadata.inputs[inputName];
|
||||
|
||||
if (!inputMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (inputMetadata.type.defaultUnit) {
|
||||
return {
|
||||
value: inputMetadata.default,
|
||||
unit: inputMetadata.type.defaultUnit
|
||||
};
|
||||
}
|
||||
|
||||
return inputMetadata.default;
|
||||
};
|
||||
|
||||
module.exports = NodeContext;
|
||||
333
packages/noodl-runtime/src/nodedefinition.js
Normal file
333
packages/noodl-runtime/src/nodedefinition.js
Normal file
@@ -0,0 +1,333 @@
|
||||
const Node = require('./node'),
|
||||
EdgeTriggeredInput = require('./edgetriggeredinput');
|
||||
|
||||
function registerInput(object, metadata, name, input) {
|
||||
if (object.hasOwnProperty(name)) {
|
||||
throw new Error('Input property ' + name + ' already registered');
|
||||
}
|
||||
|
||||
if (!input.set && !input.valueChangedToTrue) {
|
||||
input.set = () => {};
|
||||
}
|
||||
|
||||
if (input.set) {
|
||||
object[name] = {
|
||||
set: input.set
|
||||
};
|
||||
|
||||
//types to keep in the input on the node instances
|
||||
//color and textStyles are used for style updates
|
||||
//array is for supporting eval:ing strings
|
||||
const typesToSaveInInput = ['color', 'textStyle', 'array'];
|
||||
|
||||
typesToSaveInInput.forEach((type) => {
|
||||
if (input.type && (input.type === type || input.type.name === type)) {
|
||||
object[name].type = type;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (input.setUnitType) {
|
||||
object[name].setUnitType = input.setUnitType;
|
||||
}
|
||||
|
||||
metadata.inputs[name] = {
|
||||
displayName: input.displayName,
|
||||
editorName: input.editorName,
|
||||
group: input.group,
|
||||
type: input.type,
|
||||
default: input.default,
|
||||
index: input.index,
|
||||
exportToEditor: input.hasOwnProperty('exportToEditor') ? input.exportToEditor : true,
|
||||
inputPriority: input.inputPriority || 0,
|
||||
tooltip: input.tooltip,
|
||||
tab: input.tab,
|
||||
popout: input.popout,
|
||||
allowVisualStates: input.allowVisualStates,
|
||||
nodeDoubleClickAction: input.nodeDoubleClickAction
|
||||
};
|
||||
|
||||
if (input.valueChangedToTrue) {
|
||||
metadata.inputs[name].type = {
|
||||
name: 'signal',
|
||||
allowConnectionsOnly: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function registerInputs(object, metadata, inputs) {
|
||||
Object.keys(inputs).forEach(function (inputName) {
|
||||
registerInput(object, metadata, inputName, inputs[inputName]);
|
||||
});
|
||||
}
|
||||
|
||||
function registerNumberedInputs(node, numberedInputs) {
|
||||
for (const inputName of Object.keys(numberedInputs)) {
|
||||
registerNumberedInput(node, inputName, numberedInputs[inputName]);
|
||||
}
|
||||
}
|
||||
|
||||
function registerNumberedInput(node, name, input) {
|
||||
const oldRegisterInputIfNeeded = node.registerInputIfNeeded;
|
||||
|
||||
node.registerInputIfNeeded = function (inputName) {
|
||||
if (oldRegisterInputIfNeeded) {
|
||||
oldRegisterInputIfNeeded.call(node, inputName);
|
||||
}
|
||||
|
||||
if (node.hasInput(inputName) || !inputName.startsWith(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Number(inputName.slice(name.length + 1)); // inputName is "nameOfInput xxx" where xxx is the index
|
||||
|
||||
node.registerInput(inputName, {
|
||||
type: input.type,
|
||||
set: input.createSetter.call(node, index)
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function registerOutputsMetadata(metadata, outputs) {
|
||||
Object.keys(outputs).forEach(function (name) {
|
||||
var output = outputs[name];
|
||||
|
||||
metadata.outputs[name] = {
|
||||
displayName: output.displayName,
|
||||
editorName: output.editorName,
|
||||
group: output.group,
|
||||
type: output.type,
|
||||
index: output.index,
|
||||
exportToEditor: output.hasOwnProperty('exportToEditor') ? output.exportToEditor : true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function initializeDefaultValues(defaultValues, inputsMetadata) {
|
||||
Object.keys(inputsMetadata).forEach((name) => {
|
||||
const defaultValue = inputsMetadata[name].default;
|
||||
if (defaultValue === undefined) return;
|
||||
|
||||
if (inputsMetadata[name].type.defaultUnit) {
|
||||
defaultValues[name] = {
|
||||
unit: inputsMetadata[name].type.defaultUnit,
|
||||
value: defaultValue
|
||||
};
|
||||
} else {
|
||||
defaultValues[name] = defaultValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function defineNode(opts) {
|
||||
if (!opts.category) {
|
||||
throw new Error('Node must have a category');
|
||||
}
|
||||
|
||||
if (!opts.name) {
|
||||
throw new Error('Node must have a name');
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
category: opts.category,
|
||||
dynamicports: opts.dynamicports,
|
||||
exportDynamicPorts: opts.exportDynamicPorts,
|
||||
useVariants: opts.useVariants,
|
||||
allowChildren: opts.allowChildren,
|
||||
allowChildrenWithCategory: opts.allowChildrenWithCategory,
|
||||
singleton: opts.singleton,
|
||||
connectionPanel: opts.connectionPanel,
|
||||
allowAsChild: opts.allowAsChild,
|
||||
visualStates: opts.visualStates,
|
||||
panels: opts.panels,
|
||||
color: opts.color,
|
||||
usePortAsLabel: opts.usePortAsLabel,
|
||||
portLabelTruncationMode: opts.portLabelTruncationMode,
|
||||
name: opts.name,
|
||||
displayNodeName: opts.displayNodeName || opts.displayName,
|
||||
deprecated: opts.deprecated,
|
||||
haveComponentPorts: opts.haveComponentPorts,
|
||||
version: opts.version,
|
||||
module: opts.module,
|
||||
docs: opts.docs,
|
||||
allowAsExportRoot: opts.allowAsExportRoot,
|
||||
nodeDoubleClickAction: opts.nodeDoubleClickAction,
|
||||
searchTags: opts.searchTags
|
||||
};
|
||||
|
||||
opts._internal = opts._internal || {};
|
||||
|
||||
//prototypeExtensions - old API
|
||||
//methods - new API
|
||||
opts.prototypeExtensions = opts.methods || opts.prototypeExtensions || {};
|
||||
opts.inputs = opts.inputs || {};
|
||||
opts.outputs = opts.outputs || {};
|
||||
opts.initialize = opts.initialize || function () {};
|
||||
|
||||
let inputs = {};
|
||||
|
||||
registerInputs(inputs, metadata, opts.inputs);
|
||||
registerOutputsMetadata(metadata, opts.outputs);
|
||||
function NodeConstructor(context, id) {
|
||||
Node.call(this, context, id);
|
||||
}
|
||||
|
||||
Object.keys(opts.prototypeExtensions).forEach(function (propName) {
|
||||
if (!opts.prototypeExtensions[propName].value) {
|
||||
opts.prototypeExtensions[propName] = {
|
||||
value: opts.prototypeExtensions[propName]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
NodeConstructor.prototype = Object.create(Node.prototype, opts.prototypeExtensions);
|
||||
Object.defineProperty(NodeConstructor.prototype, 'name', {
|
||||
value: opts.name
|
||||
});
|
||||
|
||||
if (opts.getInspectInfo) NodeConstructor.prototype.getInspectInfo = opts.getInspectInfo;
|
||||
if (opts.nodeScopeDidInitialize) NodeConstructor.prototype.nodeScopeDidInitialize = opts.nodeScopeDidInitialize;
|
||||
|
||||
const nodeDefinition = function (context, id, nodeScope) {
|
||||
const node = new NodeConstructor(context, id);
|
||||
|
||||
//create all inputs. Use the inputs object for setters that don't have state and can be shared
|
||||
node._inputs = Object.create(inputs);
|
||||
|
||||
//all inputs that use the valueChangedToTrue have state and need to be instanced
|
||||
Object.keys(opts.inputs).forEach(function (name) {
|
||||
var input = opts.inputs[name];
|
||||
if (input.valueChangedToTrue) {
|
||||
node._inputs[name] = {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: input.valueChangedToTrue
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(opts.outputs).forEach(function (name) {
|
||||
var output = opts.outputs[name];
|
||||
if (output.type === 'signal') {
|
||||
node.registerOutput(name, {
|
||||
getter: function () {
|
||||
//signals are always emitted as a sequence of false, true, so this getter is never used
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
node.registerOutput(name, output);
|
||||
}
|
||||
});
|
||||
|
||||
opts.numberedInputs && registerNumberedInputs(node, opts.numberedInputs);
|
||||
|
||||
node.nodeScope = nodeScope;
|
||||
initializeDefaultValues(node._inputValues, metadata.inputs);
|
||||
|
||||
opts.initialize.call(node);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
nodeDefinition.metadata = metadata;
|
||||
|
||||
if (opts.numberedInputs) registerSetupFunctionForNumberedInputs(nodeDefinition, opts.name, opts.numberedInputs);
|
||||
|
||||
return nodeDefinition;
|
||||
}
|
||||
|
||||
function registerSetupFunctionForNumberedInputs(nodeDefinition, nodeType, numberedInputs) {
|
||||
const inputNames = Object.keys(numberedInputs);
|
||||
|
||||
if (!inputNames.length) return;
|
||||
|
||||
nodeDefinition.setupNumberedInputDynamicPorts = function (context, graphModel) {
|
||||
const editorConnection = context.editorConnection;
|
||||
|
||||
if (!editorConnection || !editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function collectPorts(node, inputName, input) {
|
||||
const connections = node.component.getConnectionsTo(node.id).map((c) => c.targetPort);
|
||||
|
||||
const allPortNames = Object.keys(node.parameters).concat(connections);
|
||||
const portNames = allPortNames.filter((p) => p.startsWith(inputName + ' '));
|
||||
|
||||
//Figure out how many we need to create
|
||||
//It needs to be the highest index + 1
|
||||
//Only parameters with values are present, e.g. input 3 can be missing even if input 4 is defined
|
||||
const maxIndex = portNames.length
|
||||
? 1 + Math.max(...portNames.map((p) => Number(p.slice(inputName.length + 1))))
|
||||
: 0;
|
||||
const numPorts = maxIndex + 1;
|
||||
|
||||
const ports = [];
|
||||
|
||||
for (let i = 0; i < numPorts; i++) {
|
||||
const port = {
|
||||
name: inputName + ' ' + i,
|
||||
displayName: (input.displayPrefix || inputName) + ' ' + i,
|
||||
type: input.type,
|
||||
plug: 'input',
|
||||
group: input.group
|
||||
};
|
||||
|
||||
if (input.hasOwnProperty('index')) {
|
||||
port.index = input.index + i;
|
||||
}
|
||||
|
||||
ports.push(port);
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
||||
function updatePorts(node) {
|
||||
const ports = inputNames.map((inputName) => collectPorts(node, inputName, numberedInputs[inputName])).flat();
|
||||
editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.' + nodeType, (node) => {
|
||||
updatePorts(node);
|
||||
node.on('parameterUpdated', () => {
|
||||
updatePorts(node);
|
||||
});
|
||||
|
||||
node.on('inputConnectionAdded', () => {
|
||||
updatePorts(node);
|
||||
});
|
||||
|
||||
node.on('inputConnectionRemoved', () => {
|
||||
updatePorts(node);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function extend(obj1, obj2) {
|
||||
for (var p in obj2) {
|
||||
if (p === 'initialize' && obj1.initialize) {
|
||||
var oldInit = obj1.initialize;
|
||||
obj1.initialize = function () {
|
||||
oldInit.call(this);
|
||||
obj2.initialize.call(this);
|
||||
};
|
||||
} else if (obj2[p] && obj2[p].constructor === Object) {
|
||||
obj1[p] = extend(obj1[p] || {}, obj2[p]);
|
||||
} else if (obj2[p] && obj2[p].constructor === Array && obj1[p] && obj1[p].constructor == Array) {
|
||||
obj1[p] = obj1[p].concat(obj2[p]);
|
||||
} else {
|
||||
obj1[p] = obj2[p];
|
||||
}
|
||||
}
|
||||
return obj1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
defineNode: defineNode,
|
||||
extend: extend
|
||||
};
|
||||
621
packages/noodl-runtime/src/nodelibraryexport.js
Normal file
621
packages/noodl-runtime/src/nodelibraryexport.js
Normal file
@@ -0,0 +1,621 @@
|
||||
'use strict';
|
||||
|
||||
function formatDynamicPorts(nodeMetadata) {
|
||||
const dynamicports = [];
|
||||
|
||||
for (const dp of nodeMetadata.dynamicports) {
|
||||
if (dp.ports || dp.template || dp.port || dp.channelPort) {
|
||||
//same format as editor expects, no need to transform it
|
||||
dynamicports.push(dp);
|
||||
} else if (dp.inputs || dp.outputs) {
|
||||
//inputs and outputs is just list of names
|
||||
//need to pull the metadata from the inputs/outputs since they
|
||||
//won't be registered by the editor (it's either a regular port
|
||||
// or a dynamic port, can't register both)
|
||||
const ports = [];
|
||||
|
||||
if (dp.inputs) {
|
||||
for (const inputName of dp.inputs) {
|
||||
ports.push(formatPort(inputName, nodeMetadata.inputs[inputName], 'input'));
|
||||
}
|
||||
}
|
||||
|
||||
if (dp.outputs) {
|
||||
for (const outputName of dp.outputs) {
|
||||
ports.push(formatPort(outputName, nodeMetadata.outputs[outputName], 'output'));
|
||||
}
|
||||
}
|
||||
|
||||
const dynamicPortGroup = {
|
||||
name: dp.name || 'conditionalports/basic',
|
||||
condition: dp.condition,
|
||||
ports
|
||||
};
|
||||
|
||||
dynamicports.push(dynamicPortGroup);
|
||||
}
|
||||
}
|
||||
|
||||
return dynamicports;
|
||||
}
|
||||
|
||||
function formatPort(portName, portData, plugType) {
|
||||
var port = {
|
||||
name: portName,
|
||||
type: portData.type,
|
||||
plug: plugType
|
||||
};
|
||||
if (portData.group) {
|
||||
port.group = portData.group;
|
||||
}
|
||||
if (portData.displayName) {
|
||||
port.displayName = portData.displayName;
|
||||
}
|
||||
if (portData.description) {
|
||||
port.description = portData.description;
|
||||
}
|
||||
if (portData.editorName) {
|
||||
port.editorName = portData.editorName;
|
||||
}
|
||||
if (portData.default !== undefined) {
|
||||
port.default = portData.default;
|
||||
}
|
||||
if (portData.hasOwnProperty('index')) {
|
||||
port.index = portData.index;
|
||||
}
|
||||
if (portData.tooltip) {
|
||||
port.tooltip = portData.tooltip;
|
||||
}
|
||||
if (portData.tab) {
|
||||
port.tab = portData.tab;
|
||||
}
|
||||
if (portData.popout) {
|
||||
port.popout = portData.popout;
|
||||
}
|
||||
if (portData.allowVisualStates) {
|
||||
port.allowVisualStates = portData.allowVisualStates;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
function generateNodeLibrary(nodeRegister) {
|
||||
var obj = {
|
||||
//note: needs to include ALL types
|
||||
typecasts: [
|
||||
{
|
||||
from: 'string',
|
||||
to: ['number', 'boolean', 'image', 'color', 'enum', 'textStyle', 'dimension', 'array', 'object']
|
||||
},
|
||||
{
|
||||
from: 'boolean',
|
||||
to: ['number', 'string', 'signal']
|
||||
},
|
||||
{
|
||||
from: 'number',
|
||||
to: ['boolean', 'string', 'dimension']
|
||||
},
|
||||
{
|
||||
from: 'date',
|
||||
to: ['string']
|
||||
},
|
||||
{
|
||||
from: 'signal',
|
||||
to: ['boolean', 'number']
|
||||
},
|
||||
{
|
||||
from: 'image',
|
||||
to: []
|
||||
},
|
||||
{
|
||||
from: 'cloudfile',
|
||||
to: ['string', 'image']
|
||||
},
|
||||
{
|
||||
from: 'color',
|
||||
to: []
|
||||
},
|
||||
{
|
||||
from: 'enum',
|
||||
to: []
|
||||
},
|
||||
{
|
||||
from: 'object',
|
||||
to: []
|
||||
},
|
||||
{
|
||||
from: 'domelement',
|
||||
to: []
|
||||
},
|
||||
{
|
||||
from: 'reference',
|
||||
to: []
|
||||
},
|
||||
{
|
||||
from: 'font',
|
||||
to: []
|
||||
},
|
||||
{
|
||||
from: 'textStyle',
|
||||
to: ['string']
|
||||
},
|
||||
{
|
||||
// Collection is deprecated but supported via typecasts
|
||||
from: 'collection',
|
||||
to: ['array']
|
||||
},
|
||||
{
|
||||
from: 'array',
|
||||
to: ['collection']
|
||||
}
|
||||
],
|
||||
dynamicports: [
|
||||
{
|
||||
type: 'conditionalports',
|
||||
name: 'basic'
|
||||
},
|
||||
{
|
||||
type: 'expand',
|
||||
name: 'basic'
|
||||
}
|
||||
],
|
||||
colors: {
|
||||
nodes: {
|
||||
component: {
|
||||
base: '#643D8B',
|
||||
baseHighlighted: '#79559b',
|
||||
header: '#4E2877',
|
||||
headerHighlighted: '#643d8b',
|
||||
outline: '#4E2877',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#dbd0e4'
|
||||
},
|
||||
visual: {
|
||||
base: '#315272',
|
||||
baseHighlighted: '#4d6784',
|
||||
header: '#173E5D',
|
||||
headerHighlighted: '#315272',
|
||||
outline: '#173E5D',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#cfd5de'
|
||||
},
|
||||
data: {
|
||||
base: '#465524',
|
||||
baseHighlighted: '#5b6a37',
|
||||
header: '#314110',
|
||||
headerHighlighted: '#465524',
|
||||
outline: '#314110',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#d2d6c5'
|
||||
},
|
||||
javascript: {
|
||||
base: '#7E3660',
|
||||
baseHighlighted: '#944e74',
|
||||
header: '#67214B',
|
||||
headerHighlighted: '#7e3660',
|
||||
outline: '#67214B',
|
||||
outlineHighlighted: '#d57bab',
|
||||
text: '#e4cfd9'
|
||||
},
|
||||
default: {
|
||||
base: '#4C4F59',
|
||||
baseHighlighted: '#62656e',
|
||||
header: '#373B45',
|
||||
headerHighlighted: '#4c4f59',
|
||||
outline: '#373B45',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#d3d4d6'
|
||||
}
|
||||
},
|
||||
connections: {
|
||||
signal: {
|
||||
normal: '#006f82',
|
||||
highlighted: '#7ec2cf',
|
||||
pulsing: '#ffffff'
|
||||
},
|
||||
default: {
|
||||
normal: '#875d00',
|
||||
highlighted: '#e5ae32',
|
||||
pulsing: '#ffffff'
|
||||
}
|
||||
}
|
||||
},
|
||||
nodetypes: [
|
||||
{
|
||||
name: 'Component Children',
|
||||
shortDesc: 'This node is a placeholder for where children of this component will be inserted.',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/component-children',
|
||||
color: 'component',
|
||||
allowAsChild: true,
|
||||
category: 'Visual',
|
||||
haveComponentChildren: ['Visual']
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var nodeTypes = Object.keys(nodeRegister._constructors);
|
||||
|
||||
nodeTypes.forEach(function (type) {
|
||||
var nodeMetadata = nodeRegister._constructors[type].metadata;
|
||||
|
||||
var nodeObj = {
|
||||
name: type,
|
||||
searchTags: nodeMetadata.searchTags
|
||||
};
|
||||
obj.nodetypes.push(nodeObj);
|
||||
|
||||
if (nodeMetadata.version) {
|
||||
nodeObj.version = nodeMetadata.version;
|
||||
}
|
||||
if (nodeMetadata.displayNodeName) {
|
||||
nodeObj.displayNodeName = nodeMetadata.displayNodeName;
|
||||
}
|
||||
if (nodeMetadata.nodeDoubleClickAction) {
|
||||
nodeObj.nodeDoubleClickAction = nodeMetadata.nodeDoubleClickAction;
|
||||
}
|
||||
if (nodeMetadata.shortDesc) {
|
||||
nodeObj.shortDesc = nodeMetadata.shortDesc;
|
||||
}
|
||||
if (nodeMetadata.module) {
|
||||
nodeObj.module = nodeMetadata.module;
|
||||
}
|
||||
if (nodeMetadata.deprecated) {
|
||||
nodeObj.deprecated = true;
|
||||
}
|
||||
if (nodeMetadata.haveComponentPorts) {
|
||||
nodeObj.haveComponentPorts = true;
|
||||
}
|
||||
if (nodeMetadata.category === 'Visual') {
|
||||
nodeObj.allowAsChild = true;
|
||||
nodeObj.allowAsExportRoot = true;
|
||||
nodeObj.color = 'visual';
|
||||
}
|
||||
|
||||
if (nodeMetadata.allowAsExportRoot !== undefined) {
|
||||
nodeObj.allowAsExportRoot = nodeMetadata.allowAsExportRoot;
|
||||
}
|
||||
|
||||
if (nodeMetadata.allowChildren) {
|
||||
nodeObj.allowChildrenWithCategory = ['Visual'];
|
||||
nodeObj.color = 'visual';
|
||||
}
|
||||
if (nodeMetadata.allowChildrenWithCategory) {
|
||||
nodeObj.allowChildrenWithCategory = nodeMetadata.allowChildrenWithCategory;
|
||||
}
|
||||
if (nodeMetadata.singleton) {
|
||||
nodeObj.singleton = true;
|
||||
}
|
||||
if (nodeMetadata.allowAsChild) {
|
||||
nodeObj.allowAsChild = true;
|
||||
}
|
||||
if (nodeMetadata.docs) {
|
||||
nodeObj.docs = nodeMetadata.docs;
|
||||
}
|
||||
if (nodeMetadata.shortDocs) {
|
||||
nodeObj.shortDocs = nodeMetadata.shortDocs;
|
||||
} else if (nodeMetadata.docs && nodeMetadata.docs.indexOf('https://docs.noodl.net') === 0) {
|
||||
nodeObj.shortDocs = nodeMetadata.docs.replace('/#', '') + '-short.md';
|
||||
}
|
||||
nodeObj.category = nodeMetadata.category;
|
||||
|
||||
if (nodeMetadata.panels) {
|
||||
nodeObj.panels = nodeMetadata.panels;
|
||||
}
|
||||
if (nodeMetadata.usePortAsLabel) {
|
||||
nodeObj.usePortAsLabel = nodeMetadata.usePortAsLabel;
|
||||
nodeObj.portLabelTruncationMode = nodeMetadata.portLabelTruncationMode;
|
||||
}
|
||||
if (nodeMetadata.color) {
|
||||
nodeObj.color = nodeMetadata.color;
|
||||
}
|
||||
if (nodeMetadata.dynamicports) {
|
||||
nodeObj.dynamicports = formatDynamicPorts(nodeMetadata);
|
||||
}
|
||||
if (nodeMetadata.exportDynamicPorts) {
|
||||
nodeObj.exportDynamicPorts = nodeMetadata.exportDynamicPorts;
|
||||
}
|
||||
if (nodeMetadata.visualStates) {
|
||||
nodeObj.visualStates = nodeMetadata.visualStates;
|
||||
}
|
||||
if (nodeMetadata.useVariants) {
|
||||
nodeObj.useVariants = nodeMetadata.useVariants;
|
||||
}
|
||||
if (nodeMetadata.connectionPanel) {
|
||||
nodeObj.connectionPanel = nodeMetadata.connectionPanel;
|
||||
}
|
||||
nodeObj.ports = [];
|
||||
|
||||
var dynamicports = nodeObj.dynamicports || [];
|
||||
var selectorNames = {};
|
||||
var conditionalPortNames = {};
|
||||
|
||||
//flag conditional ports so they don't get added from the normal ports, making them appear twice in the export
|
||||
/* dynamicports.filter(d=> d.name === 'conditionalports/basic')
|
||||
.forEach(d=> {
|
||||
d.ports.forEach(port=> {
|
||||
conditionalPortNames[port.plug + '/' + port.name] = true;
|
||||
});
|
||||
});*/
|
||||
|
||||
//same for channel ports
|
||||
dynamicports
|
||||
.filter((d) => d.channelPort !== undefined)
|
||||
.forEach((port) => {
|
||||
conditionalPortNames[port.channelPort.plug + '/' + port.channelPort.name] = true;
|
||||
});
|
||||
|
||||
if (dynamicports.length) {
|
||||
nodeObj.dynamicports = dynamicports;
|
||||
}
|
||||
|
||||
Object.keys(nodeMetadata.inputs).forEach(function (inputName) {
|
||||
if (
|
||||
selectorNames.hasOwnProperty('input/' + inputName) ||
|
||||
conditionalPortNames.hasOwnProperty('input/' + inputName)
|
||||
) {
|
||||
//this is a selector or dynamic port. It's already been registered
|
||||
return;
|
||||
}
|
||||
var port = nodeMetadata.inputs[inputName];
|
||||
if (port.exportToEditor === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodeObj.ports.push(formatPort(inputName, port, 'input'));
|
||||
});
|
||||
|
||||
function exportOutput(name, output) {
|
||||
var port = {
|
||||
name: name,
|
||||
type: output.type,
|
||||
plug: 'output'
|
||||
};
|
||||
if (output.group) {
|
||||
port.group = output.group;
|
||||
}
|
||||
if (output.displayName) {
|
||||
port.displayName = output.displayName;
|
||||
}
|
||||
if (output.editorName) {
|
||||
port.editorName = output.editorName;
|
||||
}
|
||||
if (output.hasOwnProperty('index')) {
|
||||
port.index = output.index;
|
||||
}
|
||||
nodeObj.ports.push(port);
|
||||
}
|
||||
|
||||
Object.keys(nodeMetadata.outputs).forEach(function (prop) {
|
||||
if (selectorNames.hasOwnProperty('output/' + prop) || conditionalPortNames.hasOwnProperty('output/' + prop)) {
|
||||
//this is a selector or dynamic port. It's already been registered
|
||||
return;
|
||||
}
|
||||
|
||||
var output = nodeMetadata.outputs[prop];
|
||||
exportOutput(prop, output);
|
||||
});
|
||||
});
|
||||
|
||||
const coreNodes = [
|
||||
{
|
||||
name: 'UI Elements',
|
||||
description: 'Buttons, inputs, containers, media',
|
||||
type: 'visual',
|
||||
subCategories: [
|
||||
{
|
||||
name: 'Basic Elements',
|
||||
items: ['Group', 'net.noodl.visual.columns', 'Text', 'Image', 'Video', 'Circle', 'net.noodl.visual.icon']
|
||||
},
|
||||
{
|
||||
name: 'UI Controls',
|
||||
items: [
|
||||
'net.noodl.controls.button',
|
||||
'net.noodl.controls.checkbox',
|
||||
'net.noodl.controls.options',
|
||||
'net.noodl.controls.radiobutton',
|
||||
'Radio Button Group',
|
||||
'net.noodl.controls.range',
|
||||
'net.noodl.controls.textinput'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Navigation & Popups',
|
||||
description: 'Page routing, navigation, popups',
|
||||
type: 'logic',
|
||||
subCategories: [
|
||||
{
|
||||
name: 'Navigation',
|
||||
items: ['Router', 'RouterNavigate', 'PageInputs', 'net.noodl.externallink', 'PageStackNavigateToPath']
|
||||
},
|
||||
{
|
||||
name: 'Component Stack',
|
||||
items: ['Page Stack', 'PageStackNavigate', 'PageStackNavigateBack']
|
||||
},
|
||||
{
|
||||
name: 'Popups',
|
||||
items: ['NavigationShowPopup', 'NavigationClosePopup']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Logic & Utilities',
|
||||
description: 'Logic, events, string manipulation',
|
||||
type: 'logic',
|
||||
subCategories: [
|
||||
{
|
||||
name: 'General Utils',
|
||||
items: [
|
||||
'States',
|
||||
'Value Changed',
|
||||
'Timer',
|
||||
'Color Blend',
|
||||
'Number Remapper',
|
||||
'Counter',
|
||||
'Drag',
|
||||
'net.noodl.animatetovalue'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Logic',
|
||||
items: ['Boolean To String', 'Switch', 'And', 'Or', 'Condition', 'Inverter']
|
||||
},
|
||||
{
|
||||
name: 'Events',
|
||||
items: ['Event Sender', 'Event Receiver']
|
||||
},
|
||||
{
|
||||
name: 'String Manipulation',
|
||||
items: ['Substring', 'String Mapper', 'String Format', 'Date To String', 'Unique Id']
|
||||
},
|
||||
{
|
||||
name: 'System',
|
||||
items: ['Screen Resolution', 'Open File Picker']
|
||||
},
|
||||
{
|
||||
name: 'Variables',
|
||||
items: ['String', 'Boolean', 'Color', 'Number']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Component Utilities',
|
||||
description: 'Component inputs, outputs & object',
|
||||
type: 'component',
|
||||
subCategories: [
|
||||
{
|
||||
name: '',
|
||||
items: [
|
||||
'Component Inputs',
|
||||
'Component Outputs',
|
||||
'Component Children',
|
||||
'net.noodl.ComponentObject',
|
||||
'net.noodl.ParentComponentObject',
|
||||
'net.noodl.SetComponentObjectProperties',
|
||||
'net.noodl.SetParentComponentObjectProperties'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Read & Write Data',
|
||||
description: 'Arrays, objects, cloud data',
|
||||
type: 'data',
|
||||
subCategories: [
|
||||
{
|
||||
name: '',
|
||||
items: [
|
||||
'RunTasks',
|
||||
'For Each',
|
||||
'For Each Actions',
|
||||
'Model2',
|
||||
'SetModelProperties',
|
||||
'NewModel',
|
||||
'Set Variable',
|
||||
'Variable2'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Array',
|
||||
items: [
|
||||
'Collection2',
|
||||
'CollectionNew',
|
||||
'CollectionRemove',
|
||||
'CollectionClear',
|
||||
'CollectionInsert',
|
||||
'Filter Collection',
|
||||
'Map Collection',
|
||||
'Static Data'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Cloud Data',
|
||||
items: [
|
||||
'DbModel2',
|
||||
'NewDbModelProperties',
|
||||
'FilterDBModels',
|
||||
'SetDbModelProperties',
|
||||
'DbCollection2',
|
||||
'DeleteDbModelProperties',
|
||||
'AddDbModelRelation',
|
||||
'RemoveDbModelRelation',
|
||||
'Cloud File',
|
||||
'Upload File',
|
||||
'CloudFunction2',
|
||||
'DbConfig'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
items: [
|
||||
'net.noodl.user.LogIn',
|
||||
'net.noodl.user.LogOut',
|
||||
'net.noodl.user.SignUp',
|
||||
'net.noodl.user.User',
|
||||
'net.noodl.user.SetUserProperties',
|
||||
'net.noodl.user.VerifyEmail',
|
||||
'net.noodl.user.SendEmailVerification',
|
||||
'net.noodl.user.ResetPassword',
|
||||
'net.noodl.user.RequestPasswordReset'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'External Data',
|
||||
items: ['REST2']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Custom Code',
|
||||
description: 'Custom JavaScript and CSS',
|
||||
type: 'javascript',
|
||||
subCategories: [
|
||||
{
|
||||
name: '',
|
||||
items: ['Expression', 'JavaScriptFunction', 'Javascript2', 'CSS Definition']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Cloud Functions',
|
||||
description: 'Nodes to be used in cloud functions',
|
||||
type: 'data',
|
||||
subCategories: [
|
||||
{
|
||||
name: '',
|
||||
items: ['noodl.cloud.request', 'noodl.cloud.response']
|
||||
},
|
||||
{
|
||||
name: 'Cloud Data',
|
||||
items: ['noodl.cloud.aggregate']
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
obj.nodeIndex = {
|
||||
coreNodes
|
||||
};
|
||||
|
||||
const moduleNodes = [];
|
||||
|
||||
nodeTypes.forEach((type) => {
|
||||
const nodeMetadata = nodeRegister._constructors[type].metadata;
|
||||
if (nodeMetadata.module) {
|
||||
moduleNodes.push(type);
|
||||
}
|
||||
});
|
||||
|
||||
if (moduleNodes.length) {
|
||||
obj.nodeIndex.moduleNodes = [
|
||||
{
|
||||
name: '',
|
||||
items: moduleNodes
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
module.exports = generateNodeLibrary;
|
||||
39
packages/noodl-runtime/src/noderegister.js
Normal file
39
packages/noodl-runtime/src/noderegister.js
Normal file
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
function NodeRegister(context) {
|
||||
this._constructors = {};
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
NodeRegister.prototype.register = function(nodeDefinition) {
|
||||
var name = nodeDefinition.metadata.name;
|
||||
|
||||
this._constructors[name] = nodeDefinition;
|
||||
};
|
||||
|
||||
NodeRegister.prototype.createNode = function(name, id, nodeScope) {
|
||||
if(this._constructors.hasOwnProperty(name) === false) {
|
||||
throw new Error("Unknown node type with name " + name);
|
||||
}
|
||||
|
||||
return this._constructors[name](this.context, id, nodeScope);
|
||||
};
|
||||
|
||||
NodeRegister.prototype.getNodeMetadata = function(type) {
|
||||
if(this._constructors.hasOwnProperty(type) === false) {
|
||||
throw new Error("Unknown node type with name " + type);
|
||||
}
|
||||
|
||||
return this._constructors[type].metadata;
|
||||
};
|
||||
|
||||
NodeRegister.prototype.hasNode = function(type) {
|
||||
return this._constructors.hasOwnProperty(type);
|
||||
};
|
||||
|
||||
NodeRegister.prototype.getInputType = function(type, inputName) {
|
||||
const metadata = this.getNodeMetadata(type);
|
||||
return metadata.inputs[inputName] && metadata.inputs[inputName].type;
|
||||
}
|
||||
|
||||
module.exports = NodeRegister;
|
||||
47
packages/noodl-runtime/src/nodes/componentinputs.js
Normal file
47
packages/noodl-runtime/src/nodes/componentinputs.js
Normal file
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
node: {
|
||||
name: 'Component Inputs',
|
||||
shortDesc: 'This node is used to define the inputs of a component.',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/component-inputs',
|
||||
panels: [
|
||||
{
|
||||
name: 'PortEditor',
|
||||
context: ['select', 'connectFrom'],
|
||||
title: 'Inputs',
|
||||
plug: 'output',
|
||||
type: {
|
||||
name: '*'
|
||||
},
|
||||
canArrangeInGroups: true
|
||||
},
|
||||
{
|
||||
name: 'PropertyEditor',
|
||||
hidden: true
|
||||
}
|
||||
],
|
||||
getInspectInfo() {
|
||||
return { type: 'value', value: this.nodeScope.componentOwner._internal.inputValues };
|
||||
},
|
||||
color: 'component',
|
||||
haveComponentPorts: true,
|
||||
category: 'Component Utilities',
|
||||
methods: {
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerOutput(name, {
|
||||
getter: function () {
|
||||
return this.nodeScope.componentOwner._internal.inputValues[name];
|
||||
}
|
||||
});
|
||||
},
|
||||
_updateDependencies: function () {
|
||||
this.nodeScope.componentOwner.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
334
packages/noodl-runtime/src/nodes/componentinstance.js
Normal file
334
packages/noodl-runtime/src/nodes/componentinstance.js
Normal file
@@ -0,0 +1,334 @@
|
||||
'use strict';
|
||||
|
||||
var Node = require('../node');
|
||||
var NodeScope = require('../nodescope');
|
||||
|
||||
let componentIdCounter = 0;
|
||||
|
||||
function ComponentInstanceNode(context, id, parentNodeScope) {
|
||||
Node.call(this, context, id);
|
||||
|
||||
this.nodeScope = new NodeScope(context, this);
|
||||
this.parentNodeScope = parentNodeScope;
|
||||
this._internal.childRoot = null;
|
||||
this._internal.componentOutputValues = {};
|
||||
this._internal.componentOutputs = [];
|
||||
this._internal.componentInputs = [];
|
||||
this._internal.inputValues = {};
|
||||
this._internal.roots = [];
|
||||
|
||||
this._internal.instanceId = '__$ndl_componentInstaceId' + componentIdCounter;
|
||||
|
||||
this.nodeScope.modelScope = parentNodeScope ? parentNodeScope.modelScope : undefined;
|
||||
|
||||
componentIdCounter++;
|
||||
}
|
||||
|
||||
ComponentInstanceNode.prototype = Object.create(Node.prototype, {
|
||||
setComponentModel: {
|
||||
value: async function (componentModel) {
|
||||
this.componentModel = componentModel;
|
||||
var self = this;
|
||||
|
||||
await this.nodeScope.setComponentModel(componentModel);
|
||||
|
||||
this._internal.componentInputs = this.nodeScope.getNodesWithType('Component Inputs');
|
||||
this._internal.componentOutputs = this.nodeScope.getNodesWithType('Component Outputs');
|
||||
|
||||
Object.values(componentModel.getInputPorts()).forEach(this.registerComponentInputPort.bind(this));
|
||||
Object.values(componentModel.getOutputPorts()).forEach(this.registerComponentOutputPort.bind(this));
|
||||
|
||||
const roots = componentModel.roots || [];
|
||||
this._internal.roots = roots.map((id) => this.nodeScope.getNodeWithId(id));
|
||||
|
||||
componentModel.on(
|
||||
'rootAdded',
|
||||
(id) => {
|
||||
this._internal.roots.push(this.nodeScope.getNodeWithId(id));
|
||||
this.forceUpdate();
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
componentModel.on(
|
||||
'rootRemoved',
|
||||
function (id) {
|
||||
const index = this._internal.roots.findIndex((root) => root.id === id);
|
||||
if (index !== -1) {
|
||||
this._internal.roots.splice(index, 1);
|
||||
}
|
||||
this.forceUpdate();
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
componentModel.on('inputPortAdded', this.registerComponentInputPort.bind(this), this);
|
||||
componentModel.on('outputPortAdded', this.registerComponentOutputPort.bind(this), this);
|
||||
|
||||
componentModel.on(
|
||||
'inputPortRemoved',
|
||||
function (port) {
|
||||
if (self.hasInput(port.name)) {
|
||||
self.deregisterInput(port.name);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
componentModel.on(
|
||||
'outputPortRemoved',
|
||||
function (port) {
|
||||
if (this.hasOutput(port.name)) {
|
||||
self.deregisterOutput(port.name);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
componentModel.on(
|
||||
'nodeAdded',
|
||||
function (node) {
|
||||
if (node.type === 'Component Inputs') {
|
||||
self._internal.componentInputs.push(self.nodeScope.getNodeWithId(node.id));
|
||||
} else if (node.type === 'Component Outputs') {
|
||||
self._internal.componentOutputs.push(self.nodeScope.getNodeWithId(node.id));
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
componentModel.on(
|
||||
'nodeRemoved',
|
||||
function (node) {
|
||||
function removeNodesWithId(array, id) {
|
||||
return array.filter((e) => e.id !== id);
|
||||
}
|
||||
if (node.type === 'Component Inputs') {
|
||||
self._internal.componentInputs = removeNodesWithId(self._internal.componentInputs, node.id);
|
||||
} else if (node.type === 'Component Outputs') {
|
||||
self._internal.componentOutputs = removeNodesWithId(self._internal.componentOutputs, node.id);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
componentModel.on(
|
||||
'renamed',
|
||||
function (event) {
|
||||
self.name = event.newName;
|
||||
},
|
||||
this
|
||||
);
|
||||
}
|
||||
},
|
||||
_onNodeDeleted: {
|
||||
value: function () {
|
||||
if (this.componentModel) {
|
||||
this.componentModel.removeListenersWithRef(this);
|
||||
this.componentModel = undefined;
|
||||
}
|
||||
|
||||
this.nodeScope.reset();
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
}
|
||||
},
|
||||
registerComponentInputPort: {
|
||||
value: function (port) {
|
||||
this.registerInput(port.name, {
|
||||
set: function (value) {
|
||||
this._internal.inputValues[port.name] = value;
|
||||
this._internal.componentInputs.forEach(function (componentInput) {
|
||||
componentInput.registerOutputIfNeeded(port.name);
|
||||
componentInput.flagOutputDirty(port.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
registerComponentOutputPort: {
|
||||
value: function (port) {
|
||||
this.registerOutput(port.name, {
|
||||
getter: function () {
|
||||
return this._internal.componentOutputValues[port.name];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
setOutputFromComponentOutput: {
|
||||
value: function (name, value) {
|
||||
if (this.hasOutput(name) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.creatorCallbacks &&
|
||||
this._internal.creatorCallbacks.onOutputChanged &&
|
||||
this._internal.creatorCallbacks.onOutputChanged(name, value, this._internal.componentOutputValues[name]);
|
||||
|
||||
this._internal.componentOutputValues[name] = value;
|
||||
this.flagOutputDirty(name);
|
||||
}
|
||||
},
|
||||
setChildRoot: {
|
||||
value: function (node) {
|
||||
const prevChildRoot = this._internal.childRoot;
|
||||
const newChildRoot = node;
|
||||
|
||||
this._internal.childRoot = newChildRoot;
|
||||
|
||||
if (this.model && this.model.children) {
|
||||
const parentNodeScope = this.parentNodeScope;
|
||||
|
||||
const children = this.model.children
|
||||
.filter((child) => child.type !== 'Component Children')
|
||||
.map((child) => parentNodeScope.getNodeWithId(child.id));
|
||||
|
||||
if (prevChildRoot) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (prevChildRoot.isChild(children[i])) {
|
||||
prevChildRoot.removeChild(children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newChildRoot) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
const index = child.model.parent.children.indexOf(child.model);
|
||||
this.addChild(child, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getChildRootIndex: {
|
||||
value: function () {
|
||||
if (!this._internal.childRoot || !this._internal.childRoot.model || !this._internal.childRoot.model.children) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var children = this._internal.childRoot.model.children;
|
||||
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
if (children[i].type === 'Component Children') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
getChildRoot: {
|
||||
value: function () {
|
||||
if (this._internal.childRoot) {
|
||||
return this._internal.childRoot;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getRoots: {
|
||||
value: function () {
|
||||
return this._internal.roots;
|
||||
}
|
||||
},
|
||||
/** Added for SSR Support */
|
||||
triggerDidMount: {
|
||||
value: function () {
|
||||
this._internal.roots.forEach((root) => {
|
||||
root.triggerDidMount && root.triggerDidMount();
|
||||
});
|
||||
}
|
||||
},
|
||||
render: {
|
||||
value: function () {
|
||||
if (this._internal.roots.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._internal.roots[0].render();
|
||||
}
|
||||
},
|
||||
setChildIndex: {
|
||||
value: function (childIndex) {
|
||||
// NOTE: setChildIndex can be undefined when it is not a React node,
|
||||
// but still a visual node like the foreach (Repeater) node.
|
||||
this.getRoots().forEach((root) => root.setChildIndex && root.setChildIndex(childIndex));
|
||||
}
|
||||
},
|
||||
addChild: {
|
||||
value: function (child, index) {
|
||||
this.getChildRoot().addChild(child, index + this.getChildRootIndex());
|
||||
}
|
||||
},
|
||||
removeChild: {
|
||||
value: function (child) {
|
||||
this.getChildRoot().removeChild(child);
|
||||
}
|
||||
},
|
||||
getChildren: {
|
||||
value: function (child) {
|
||||
const childRoot = this.getChildRoot();
|
||||
return childRoot ? childRoot.getChildren() : [];
|
||||
}
|
||||
},
|
||||
isChild: {
|
||||
value: function (child) {
|
||||
if (!this.getChildRoot()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.getChildRoot().isChild(child);
|
||||
}
|
||||
},
|
||||
contains: {
|
||||
value: function (node) {
|
||||
return this.getRoots().some((root) => root.contains && root.contains(node));
|
||||
}
|
||||
},
|
||||
_performDirtyUpdate: {
|
||||
value: function () {
|
||||
Node.prototype._performDirtyUpdate.call(this);
|
||||
|
||||
var componentInputs = this._internal.componentInputs;
|
||||
for (var i = 0, len = componentInputs.length; i < len; i++) {
|
||||
componentInputs[i].flagDirty();
|
||||
}
|
||||
|
||||
this._internal.componentOutputs.forEach(function (componentOutput) {
|
||||
componentOutput.flagDirty();
|
||||
});
|
||||
}
|
||||
},
|
||||
getRef: {
|
||||
value: function () {
|
||||
const root = this._internal.roots[0];
|
||||
return root ? root.getRef() : undefined;
|
||||
}
|
||||
},
|
||||
update: {
|
||||
value: function () {
|
||||
Node.prototype.update.call(this);
|
||||
|
||||
this._internal.componentOutputs.forEach(function (componentOutput) {
|
||||
componentOutput.update();
|
||||
});
|
||||
}
|
||||
},
|
||||
forceUpdate: {
|
||||
//this is only used when roots are added or removed
|
||||
value: function () {
|
||||
if (!this.parent) return;
|
||||
|
||||
//the parent will need to re-render the roots of this component instance
|
||||
//TODO: make this use a cleaner API, and only invalidate the affected child, instead of all children
|
||||
this.parent.cachedChildren = undefined;
|
||||
this.parent.forceUpdate();
|
||||
}
|
||||
},
|
||||
getInstanceId: {
|
||||
value() {
|
||||
return this._internal.instanceId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ComponentInstanceNode.prototype.constructor = ComponentInstanceNode;
|
||||
module.exports = ComponentInstanceNode;
|
||||
41
packages/noodl-runtime/src/nodes/componentoutputs.js
Normal file
41
packages/noodl-runtime/src/nodes/componentoutputs.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
node: {
|
||||
category: 'Component Utilities',
|
||||
name: 'Component Outputs',
|
||||
shortDesc: 'This node is used to define the outputs of a component.',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/component-outputs',
|
||||
panels: [
|
||||
{
|
||||
name: 'PortEditor',
|
||||
context: ['select', 'connectTo'],
|
||||
title: 'Outputs',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: '*'
|
||||
},
|
||||
canArrangeInGroups: true
|
||||
},
|
||||
{
|
||||
name: 'PropertyEditor',
|
||||
hidden: true
|
||||
}
|
||||
],
|
||||
color: 'component',
|
||||
haveComponentPorts: true,
|
||||
prototypeExtensions: {
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
this.nodeScope.componentOwner.setOutputFromComponentOutput(name, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
54
packages/noodl-runtime/src/nodes/std-library/and.js
Normal file
54
packages/noodl-runtime/src/nodes/std-library/and.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const AndNode = {
|
||||
name: 'And',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/and',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return and(this._internal.inputs);
|
||||
},
|
||||
numberedInputs: {
|
||||
input: {
|
||||
displayPrefix: 'Input',
|
||||
type: 'boolean',
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
value = value ? true : false;
|
||||
|
||||
if (this._internal.inputs[index] === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.inputs[index] = value;
|
||||
const result = and(this._internal.inputs);
|
||||
|
||||
if (this._internal.result !== result) {
|
||||
this._internal.result = result;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Result',
|
||||
get() {
|
||||
return this._internal.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: AndNode
|
||||
};
|
||||
|
||||
function and(values) {
|
||||
//if none are false, then return true
|
||||
return values.length > 0 && values.some((v) => !v) === false;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const BooleanToStringNode = {
|
||||
name: 'Boolean To String',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/boolean-to-string',
|
||||
category: 'Utilities',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
this._internal.currentSelectedIndex = 0;
|
||||
this._internal.indexChanged = false;
|
||||
|
||||
this._internal.trueString = '';
|
||||
this._internal.falseString = '';
|
||||
},
|
||||
inputs: {
|
||||
trueString: {
|
||||
displayName: 'String for true',
|
||||
type: 'string',
|
||||
set: function (value) {
|
||||
if (this._internal.trueString === value) return;
|
||||
this._internal.trueString = value;
|
||||
|
||||
if (this._internal.currentInput) {
|
||||
this.flagOutputDirty('currentValue');
|
||||
}
|
||||
}
|
||||
},
|
||||
falseString: {
|
||||
displayName: 'String for false',
|
||||
type: 'string',
|
||||
set: function (value) {
|
||||
if (this._internal.falseString === value) return;
|
||||
this._internal.falseString = value;
|
||||
|
||||
if (!this._internal.currentInput) {
|
||||
this.flagOutputDirty('currentValue');
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
type: { name: 'boolean' },
|
||||
displayName: 'Selector',
|
||||
set: function (value) {
|
||||
if (this._internal.currentInput === value) return;
|
||||
|
||||
this._internal.currentInput = value;
|
||||
this.flagOutputDirty('currentValue');
|
||||
this.sendSignalOnOutput('inputChanged');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentValue: {
|
||||
type: 'string',
|
||||
displayName: 'Current Value',
|
||||
group: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.currentInput ? this._internal.trueString : this._internal.falseString;
|
||||
}
|
||||
},
|
||||
inputChanged: {
|
||||
type: 'signal',
|
||||
displayName: 'Selector Changed',
|
||||
group: 'Signals'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: BooleanToStringNode
|
||||
};
|
||||
86
packages/noodl-runtime/src/nodes/std-library/condition.js
Normal file
86
packages/noodl-runtime/src/nodes/std-library/condition.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
const ConditionNode = {
|
||||
name: 'Condition',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/logic/condition',
|
||||
category: 'Logic',
|
||||
initialize: function () {},
|
||||
getInspectInfo() {
|
||||
const condition = this.getInputValue('condition');
|
||||
let value;
|
||||
if (condition === undefined) {
|
||||
value = '[No input]';
|
||||
}
|
||||
value = condition;
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
displayName: 'Condition',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
if (!this.isInputConnected('eval')) {
|
||||
// Evaluate right away
|
||||
this.scheduleEvaluate();
|
||||
}
|
||||
}
|
||||
},
|
||||
eval: {
|
||||
type: 'signal',
|
||||
displayName: 'Evaluate',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleEvaluate();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
ontrue: {
|
||||
type: 'signal',
|
||||
displayName: 'On True',
|
||||
group: 'Events'
|
||||
},
|
||||
onfalse: {
|
||||
type: 'signal',
|
||||
displayName: 'On False',
|
||||
group: 'Events'
|
||||
},
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Is True',
|
||||
group: 'Booleans',
|
||||
get() {
|
||||
return !!this.getInputValue('condition');
|
||||
}
|
||||
},
|
||||
isfalse: {
|
||||
type: 'boolean',
|
||||
displayName: 'Is False',
|
||||
group: 'Booleans',
|
||||
get() {
|
||||
return !this.getInputValue('condition');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleEvaluate() {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.flagOutputDirty('result');
|
||||
this.flagOutputDirty('isfalse');
|
||||
|
||||
const condition = this.getInputValue('condition');
|
||||
this.sendSignalOnOutput(condition ? 'ontrue' : 'onfalse');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ConditionNode
|
||||
};
|
||||
124
packages/noodl-runtime/src/nodes/std-library/counter.js
Normal file
124
packages/noodl-runtime/src/nodes/std-library/counter.js
Normal file
@@ -0,0 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
const CounterNode = {
|
||||
name: 'Counter',
|
||||
docs: 'https://docs.noodl.net/nodes/math/counter',
|
||||
category: 'Math',
|
||||
initialize: function () {
|
||||
this._internal.currentValue = 0;
|
||||
this._internal.startValue = 0;
|
||||
this._internal.startValueSet = false;
|
||||
|
||||
this._internal.limitsEnabled = false;
|
||||
this._internal.limitsMin = 0;
|
||||
this._internal.limitsMax = 0;
|
||||
},
|
||||
getInspectInfo() {
|
||||
return 'Count: ' + this._internal.currentValue;
|
||||
},
|
||||
inputs: {
|
||||
increase: {
|
||||
group: 'Actions',
|
||||
displayName: 'Increase Count',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.limitsEnabled && this._internal.currentValue >= this._internal.limitsMax) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.currentValue++;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
},
|
||||
decrease: {
|
||||
group: 'Actions',
|
||||
displayName: 'Decrease Count',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.limitsEnabled && this._internal.currentValue <= this._internal.limitsMin) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.currentValue--;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
group: 'Actions',
|
||||
displayName: 'Reset To Start',
|
||||
valueChangedToTrue: function () {
|
||||
if (this.currentValue === 0) {
|
||||
return;
|
||||
}
|
||||
this._internal.currentValue = this._internal.startValue;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
},
|
||||
startValue: {
|
||||
type: 'number',
|
||||
displayName: 'Start Value',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.startValue = Number(value);
|
||||
|
||||
if (this._internal.startValueSet === false) {
|
||||
this._internal.startValueSet = true;
|
||||
this._internal.currentValue = this._internal.startValue;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
}
|
||||
},
|
||||
limitsMin: {
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
displayName: 'Min Value',
|
||||
group: 'Limits',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.limitsMin = Number(value);
|
||||
}
|
||||
},
|
||||
limitsMax: {
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
displayName: 'Max Value',
|
||||
group: 'Limits',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.limitsMax = Number(value);
|
||||
}
|
||||
},
|
||||
limitsEnabled: {
|
||||
type: {
|
||||
name: 'boolean'
|
||||
},
|
||||
displayName: 'Limits Enabled',
|
||||
group: 'Limits',
|
||||
default: false,
|
||||
set: function (value) {
|
||||
this._internal.limitsEnabled = value ? true : false;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentCount: {
|
||||
displayName: 'Current Count',
|
||||
type: 'number',
|
||||
getter: function () {
|
||||
return this._internal.currentValue;
|
||||
}
|
||||
},
|
||||
countChanged: {
|
||||
displayName: 'Count Changed',
|
||||
type: 'signal'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CounterNode
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
const CloudFile = require('../../../api/cloudfile');
|
||||
|
||||
const CloudFileNode = {
|
||||
name: 'Cloud File',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/cloud-file',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
getInspectInfo() {
|
||||
return this._internal.cloudFile && this._internal.cloudFile.getUrl();
|
||||
},
|
||||
outputs: {
|
||||
url: {
|
||||
type: 'string',
|
||||
displayName: 'URL',
|
||||
group: 'General',
|
||||
get() {
|
||||
return this._internal.cloudFile && this._internal.cloudFile.getUrl();
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
displayName: 'Name',
|
||||
group: 'General',
|
||||
get() {
|
||||
if (!this._internal.cloudFile) return;
|
||||
|
||||
//parse prefixes the file with a guid_
|
||||
//remove it so the name is the same as the original file name
|
||||
const n = this._internal.cloudFile.getName().split('_');
|
||||
return n.length === 1 ? n[0] : n.slice(1).join('_');
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
file: {
|
||||
type: 'cloudfile',
|
||||
displayName: 'Cloud File',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
if (value instanceof CloudFile === false) {
|
||||
return;
|
||||
}
|
||||
this._internal.cloudFile = value;
|
||||
this.flagOutputDirty('name');
|
||||
this.flagOutputDirty('url');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CloudFileNode
|
||||
};
|
||||
@@ -0,0 +1,722 @@
|
||||
const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');
|
||||
|
||||
const Model = require('../../../model'),
|
||||
Collection = require('../../../collection'),
|
||||
CloudStore = require('../../../api/cloudstore'),
|
||||
JavascriptNodeParser = require('../../../javascriptnodeparser'),
|
||||
QueryUtils = require('../../../api/queryutils');
|
||||
|
||||
var DbCollectionNode = {
|
||||
name: 'DbCollection2',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/query-records',
|
||||
displayName: 'Query Records',
|
||||
/* shortDesc: "A database collection.",*/
|
||||
category: 'Cloud Services',
|
||||
usePortAsLabel: 'collectionName',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
this._internal.queryParameters = {};
|
||||
|
||||
var collectionChangedScheduled = false;
|
||||
this._internal.collectionChangedCallback = function () {
|
||||
//this can be called multiple times when adding/removing more than one item
|
||||
//so optimize by only updating outputs once
|
||||
if (collectionChangedScheduled) return;
|
||||
collectionChangedScheduled = true;
|
||||
|
||||
_this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
collectionChangedScheduled = false;
|
||||
});
|
||||
};
|
||||
|
||||
this._internal.cloudStoreEvents = function (args) {
|
||||
if (_this.isInputConnected('storageFetch') === true) return;
|
||||
|
||||
if (_this._internal.collection === undefined) return;
|
||||
if (args.collection !== _this._internal.name) return;
|
||||
|
||||
function _addModelAtCorrectIndex(m) {
|
||||
if (_this._internal.currentQuery.sort !== undefined) {
|
||||
// We need to add it at the right index
|
||||
for (var i = 0; i < _this._internal.collection.size(); i++)
|
||||
if (QueryUtils.compareObjects(_this._internal.currentQuery.sort, _this._internal.collection.get(i), m) > 0)
|
||||
break;
|
||||
|
||||
_this._internal.collection.addAtIndex(m, i);
|
||||
} else {
|
||||
_this._internal.collection.add(m);
|
||||
}
|
||||
|
||||
// Make sure we don't exceed limit
|
||||
let size = _this._internal.collection.size();
|
||||
if (_this._internal.currentQuery.limit !== undefined && size > _this._internal.currentQuery.limit)
|
||||
_this._internal.collection.remove(
|
||||
_this._internal.collection.get(
|
||||
_this._internal.currentQuery.sort !== undefined && _this._internal.currentQuery.sort[0][0] === '-'
|
||||
? size - 1
|
||||
: 0
|
||||
)
|
||||
);
|
||||
|
||||
//Send the array again over the items output to trigger function nodes etc that might be connected
|
||||
_this.flagOutputDirty('items');
|
||||
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
}
|
||||
|
||||
if (args.type === 'create') {
|
||||
const m = Model.get(args.object.objectId);
|
||||
if (m !== undefined) {
|
||||
// Check if the object matches the current query
|
||||
if (QueryUtils.matchesQuery(m, _this._internal.currentQuery.where)) {
|
||||
// If matches the query, add the item to results
|
||||
_addModelAtCorrectIndex(m);
|
||||
}
|
||||
}
|
||||
} else if (args.type === 'save') {
|
||||
const m = Model.get(args.objectId);
|
||||
if (m !== undefined) {
|
||||
const matchesQuery = QueryUtils.matchesQuery(m, _this._internal.currentQuery.where);
|
||||
|
||||
if (!matchesQuery && _this._internal.collection.contains(m)) {
|
||||
// The record no longer matches the filter, remove it
|
||||
_this._internal.collection.remove(m);
|
||||
|
||||
//Send the array again over the items output to trigger function nodes etc that might be connected
|
||||
_this.flagOutputDirty('items');
|
||||
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
} else if (matchesQuery && !_this._internal.collection.contains(m)) {
|
||||
// It's not part of the result collection but now matches they query, add it and resort
|
||||
_addModelAtCorrectIndex(m);
|
||||
}
|
||||
}
|
||||
} else if (args.type === 'delete') {
|
||||
const m = Model.get(args.objectId);
|
||||
if (m !== undefined) {
|
||||
_this._internal.collection.remove(m);
|
||||
|
||||
//Send the array again over the items output to trigger function nodes etc that might be connected
|
||||
_this.flagOutputDirty('items');
|
||||
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listening to cloud store events is only for the global model scope, only valid in browser
|
||||
// in cloud runtime its a nop
|
||||
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
|
||||
cloudstore.on('save', this._internal.cloudStoreEvents);
|
||||
cloudstore.on('create', this._internal.cloudStoreEvents);
|
||||
cloudstore.on('delete', this._internal.cloudStoreEvents);
|
||||
|
||||
this._internal.storageSettings = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
const collection = this._internal.collection;
|
||||
if (!collection) {
|
||||
return { type: 'text', value: '[Not executed yet]' };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value: collection.items
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.collection;
|
||||
}
|
||||
},
|
||||
firstItemId: {
|
||||
type: 'string',
|
||||
displayName: 'First Record Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if (this._internal.collection) {
|
||||
var firstItem = this._internal.collection.get(0);
|
||||
if (firstItem !== undefined) return firstItem.getId();
|
||||
}
|
||||
}
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
displayName: 'Count',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.collection ? this._internal.collection.size() : 0;
|
||||
}
|
||||
},
|
||||
fetched: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Success'
|
||||
},
|
||||
failure: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Failure'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setCollectionName: function (name) {
|
||||
this._internal.name = name;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
setCollection: function (collection) {
|
||||
this.bindCollection(collection);
|
||||
this.flagOutputDirty('firstItemId');
|
||||
this.flagOutputDirty('items');
|
||||
this.flagOutputDirty('count');
|
||||
},
|
||||
unbindCurrentCollection: function () {
|
||||
var collection = this._internal.collection;
|
||||
if (!collection) return;
|
||||
collection.off('change', this._internal.collectionChangedCallback);
|
||||
this._internal.collection = undefined;
|
||||
},
|
||||
bindCollection: function (collection) {
|
||||
this.unbindCurrentCollection();
|
||||
this._internal.collection = collection;
|
||||
collection && collection.on('change', this._internal.collectionChangedCallback);
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this.unbindCurrentCollection();
|
||||
|
||||
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
|
||||
cloudstore.off('insert', this._internal.cloudStoreEvents);
|
||||
cloudstore.off('delete', this._internal.cloudStoreEvents);
|
||||
cloudstore.off('save', this._internal.cloudStoreEvents);
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.err = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
if (internal.fetchScheduled) return;
|
||||
internal.fetchScheduled = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
internal.fetchScheduled = false;
|
||||
|
||||
this.fetch();
|
||||
});
|
||||
},
|
||||
fetch: function () {
|
||||
if (this.context.editorConnection) {
|
||||
if (this._internal.name === undefined) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection', {
|
||||
message: 'No collection specified for query'
|
||||
});
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection');
|
||||
}
|
||||
}
|
||||
|
||||
const _c = Collection.get();
|
||||
const f = this.getStorageFilter();
|
||||
const limit = this.getStorageLimit();
|
||||
const skip = this.getStorageSkip();
|
||||
const count = this.getStorageFetchTotalCount();
|
||||
this._internal.currentQuery = {
|
||||
where: f.where,
|
||||
sort: f.sort,
|
||||
limit: limit,
|
||||
skip: skip
|
||||
};
|
||||
CloudStore.forScope(this.nodeScope.modelScope).query({
|
||||
collection: this._internal.name,
|
||||
where: f.where,
|
||||
sort: f.sort,
|
||||
limit: limit,
|
||||
skip: skip,
|
||||
count: count,
|
||||
success: (results,count) => {
|
||||
if (results !== undefined) {
|
||||
_c.set(
|
||||
results.map((i) => {
|
||||
var m = CloudStore._fromJSON(i, this._internal.name, this.nodeScope.modelScope);
|
||||
|
||||
return m;
|
||||
})
|
||||
);
|
||||
}
|
||||
if(count !== undefined) {
|
||||
this._internal.storageSettings.storageTotalCount = count;
|
||||
if(this.hasOutput('storageTotalCount'))
|
||||
this.flagOutputDirty('storageTotalCount');
|
||||
}
|
||||
this.setCollection(_c);
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
error: (err) => {
|
||||
this.setCollection(_c);
|
||||
this.setError(err || 'Failed to fetch.');
|
||||
}
|
||||
});
|
||||
},
|
||||
getStorageFilter: function () {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
if (storageSettings['storageFilterType'] === undefined || storageSettings['storageFilterType'] === 'simple') {
|
||||
// Create simple filter
|
||||
const _where =
|
||||
this._internal.visualFilter !== undefined
|
||||
? QueryUtils.convertVisualFilter(this._internal.visualFilter, {
|
||||
queryParameters: this._internal.queryParameters,
|
||||
collectionName: this._internal.name
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const _sort =
|
||||
this._internal.visualSorting !== undefined
|
||||
? QueryUtils.convertVisualSorting(this._internal.visualSorting)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
where: _where,
|
||||
sort: _sort
|
||||
};
|
||||
} else if (storageSettings['storageFilterType'] === 'json') {
|
||||
// JSON filter
|
||||
if (!this._internal.filterFunc) {
|
||||
try {
|
||||
var filterCode = storageSettings['storageJSONFilter'];
|
||||
|
||||
// Parse out variables
|
||||
filterCode = filterCode.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
|
||||
this._internal.filterVariables = filterCode.match(/\$[A-Za-z0-9]+/g) || [];
|
||||
|
||||
var args = ['filter', 'where', 'sort', 'Inputs']
|
||||
.concat(this._internal.filterVariables)
|
||||
.concat([filterCode]);
|
||||
this._internal.filterFunc = Function.apply(null, args);
|
||||
} catch (e) {
|
||||
this._internal.filterFunc = undefined;
|
||||
console.log('Error while parsing filter script: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._internal.filterFunc) return;
|
||||
|
||||
var _filter = {},
|
||||
_sort = [],
|
||||
_this = this;
|
||||
|
||||
// Collect filter variables
|
||||
var _filterCb = function (f) {
|
||||
_filter = QueryUtils.convertFilterOp(f, {
|
||||
collectionName: _this._internal.name,
|
||||
error: function (err) {
|
||||
_this.context.editorConnection.sendWarning(
|
||||
_this.nodeScope.componentOwner.name,
|
||||
_this.id,
|
||||
'query-collection-filter',
|
||||
{
|
||||
message: err
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
var _sortCb = function (s) {
|
||||
_sort = s;
|
||||
};
|
||||
|
||||
// Extract inputs
|
||||
const inputs = {};
|
||||
for (let key in storageSettings) {
|
||||
if (key.startsWith('storageFilterValue-'))
|
||||
inputs[key.substring('storageFilterValue-'.length)] = storageSettings[key];
|
||||
}
|
||||
|
||||
var filterFuncArgs = [_filterCb, _filterCb, _sortCb, inputs]; // One for filter, one for where
|
||||
|
||||
this._internal.filterVariables.forEach((v) => {
|
||||
filterFuncArgs.push(storageSettings['storageFilterValue-' + v.substring(1)]);
|
||||
});
|
||||
|
||||
// Run the code to get the filter
|
||||
try {
|
||||
this._internal.filterFunc.apply(this, filterFuncArgs);
|
||||
} catch (e) {
|
||||
console.log('Error while running filter script: ' + e);
|
||||
}
|
||||
|
||||
return { where: _filter, sort: _sort };
|
||||
}
|
||||
},
|
||||
getStorageLimit: function () {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
|
||||
if (!storageSettings['storageEnableLimit']) return;
|
||||
else return storageSettings['storageLimit'] || 10;
|
||||
},
|
||||
getStorageSkip: function () {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
|
||||
if (!storageSettings['storageEnableLimit']) return;
|
||||
else return storageSettings['storageSkip'] || 0;
|
||||
},
|
||||
getStorageFetchTotalCount: function() {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
|
||||
return !!storageSettings['storageEnableCount'];
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerOutput(name, {
|
||||
getter: userOutputGetter.bind(this, name)
|
||||
});
|
||||
},
|
||||
setVisualFilter: function (value) {
|
||||
this._internal.visualFilter = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
setVisualSorting: function (value) {
|
||||
this._internal.visualSorting = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
setQueryParameter: function (name, value) {
|
||||
this._internal.queryParameters[name] = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('qp-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setQueryParameter.bind(this, name.substring('qp-'.length))
|
||||
});
|
||||
|
||||
const dynamicSignals = {
|
||||
storageFetch: this.scheduleFetch.bind(this)
|
||||
};
|
||||
|
||||
if (dynamicSignals[name])
|
||||
return this.registerInput(name, {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: dynamicSignals[name]
|
||||
})
|
||||
});
|
||||
|
||||
const dynamicSetters = {
|
||||
collectionName: this.setCollectionName.bind(this),
|
||||
visualFilter: this.setVisualFilter.bind(this),
|
||||
visualSort: this.setVisualSorting.bind(this)
|
||||
};
|
||||
|
||||
if (dynamicSetters[name])
|
||||
return this.registerInput(name, {
|
||||
set: dynamicSetters[name]
|
||||
});
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userOutputGetter(name) {
|
||||
/* jshint validthis:true */
|
||||
return this._internal.storageSettings[name];
|
||||
}
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
this._internal.storageSettings[name] = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
}
|
||||
|
||||
const _defaultJSONQuery =
|
||||
'// Write your query script here, check out the reference documentation for examples\n' + 'where({ })\n';
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, graphModel) {
|
||||
var ports = [];
|
||||
|
||||
const dbCollections = graphModel.getMetaData('dbCollections');
|
||||
const systemCollections = graphModel.getMetaData('systemCollections');
|
||||
|
||||
const _systemClasses = [
|
||||
{ label: 'User', value: '_User' },
|
||||
{ label: 'Role', value: '_Role' }
|
||||
];
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _systemClasses.concat(
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: []
|
||||
),
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Class',
|
||||
plug: 'input',
|
||||
group: 'General'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'storageFilterType',
|
||||
type: {
|
||||
name: 'enum',
|
||||
allowEditOnly: true,
|
||||
enums: [
|
||||
{ value: 'simple', label: 'Visual' },
|
||||
{ value: 'json', label: 'Javascript' }
|
||||
]
|
||||
},
|
||||
displayName: 'Filter',
|
||||
default: 'simple',
|
||||
plug: 'input',
|
||||
group: 'General'
|
||||
});
|
||||
|
||||
// Limit
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'storageEnableLimit',
|
||||
displayName: 'Use limit'
|
||||
});
|
||||
|
||||
if (parameters['storageEnableLimit']) {
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 10,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'storageLimit',
|
||||
displayName: 'Limit'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 0,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'storageSkip',
|
||||
displayName: 'Skip'
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'input',
|
||||
group: 'Actions',
|
||||
name: 'storageFetch',
|
||||
displayName: 'Do'
|
||||
});
|
||||
|
||||
// Total Count
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
plug: 'input',
|
||||
group: 'Total Count',
|
||||
name: 'storageEnableCount',
|
||||
displayName: 'Fetch total count'
|
||||
});
|
||||
|
||||
if (parameters['storageEnableCount']) {
|
||||
ports.push({
|
||||
type: 'number',
|
||||
plug: 'output',
|
||||
group: 'General',
|
||||
name: 'storageTotalCount',
|
||||
displayName: 'Total Count'
|
||||
});
|
||||
}
|
||||
|
||||
// Simple query
|
||||
if (parameters['storageFilterType'] === undefined || parameters['storageFilterType'] === 'simple') {
|
||||
if (parameters.collectionName !== undefined) {
|
||||
var c = dbCollections && dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections) c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
const schema = JSON.parse(JSON.stringify(c.schema));
|
||||
|
||||
// Find all records that have a relation with this type
|
||||
function _findRelations(c) {
|
||||
if (c.schema !== undefined && c.schema.properties !== undefined)
|
||||
for (var key in c.schema.properties) {
|
||||
var p = c.schema.properties[key];
|
||||
if (p.type === 'Relation' && p.targetClass === parameters.collectionName) {
|
||||
if (schema.relations === undefined) schema.relations = {};
|
||||
if (schema.relations[c.name] === undefined) schema.relations[c.name] = [];
|
||||
|
||||
schema.relations[c.name].push({ property: key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbCollections && dbCollections.forEach(_findRelations);
|
||||
systemCollections && systemCollections.forEach(_findRelations);
|
||||
|
||||
ports.push({
|
||||
name: 'visualFilter',
|
||||
plug: 'input',
|
||||
type: { name: 'query-filter', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Filter',
|
||||
group: 'Filter'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'visualSort',
|
||||
plug: 'input',
|
||||
type: { name: 'query-sorting', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Sort',
|
||||
group: 'Sorting'
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters.visualFilter !== undefined) {
|
||||
// Find all input ports
|
||||
const uniqueInputs = {};
|
||||
function _collectInputs(query) {
|
||||
if (query === undefined) return;
|
||||
if (query.rules !== undefined) query.rules.forEach((r) => _collectInputs(r));
|
||||
else if (query.input !== undefined) uniqueInputs[query.input] = true;
|
||||
}
|
||||
|
||||
_collectInputs(parameters.visualFilter);
|
||||
Object.keys(uniqueInputs).forEach((input) => {
|
||||
ports.push({
|
||||
name: 'qp-' + input,
|
||||
plug: 'input',
|
||||
type: '*',
|
||||
displayName: input,
|
||||
group: 'Query Parameters'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// JSON query
|
||||
else if (parameters['storageFilterType'] === 'json') {
|
||||
ports.push({
|
||||
type: { name: 'string', allowEditOnly: true, codeeditor: 'javascript' },
|
||||
plug: 'input',
|
||||
group: 'Filter',
|
||||
name: 'storageJSONFilter',
|
||||
default: _defaultJSONQuery,
|
||||
displayName: 'Filter'
|
||||
});
|
||||
|
||||
var filter = parameters['storageJSONFilter'];
|
||||
if (filter) {
|
||||
filter = filter.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
|
||||
var variables = filter.match(/\$[A-Za-z0-9]+/g);
|
||||
|
||||
if (variables) {
|
||||
const unique = {};
|
||||
variables.forEach((v) => {
|
||||
unique[v] = true;
|
||||
});
|
||||
|
||||
Object.keys(unique).forEach((p) => {
|
||||
ports.push({
|
||||
name: 'storageFilterValue-' + p.substring(1),
|
||||
displayName: p.substring(1),
|
||||
group: 'Filter Values',
|
||||
plug: 'input',
|
||||
type: { name: '*', allowConnectionsOnly: true }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Support variables with the "Inputs."" syntax
|
||||
JavascriptNodeParser.parseAndAddPortsFromScript(filter, ports, {
|
||||
inputPrefix: 'storageFilterValue-',
|
||||
inputGroup: 'Filter Values',
|
||||
inputType: { name: '*', allowConnectionsOnly: true },
|
||||
skipOutputs: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: DbCollectionNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name.startsWith('storage') || event.name === 'visualFilter' || event.name === 'collectionName') {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
}
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.cloudservices', function (data) {
|
||||
CloudStore.instance._initCloudServices();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.DbCollection2', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('DbCollection2')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
172
packages/noodl-runtime/src/nodes/std-library/data/dbconfig.js
Normal file
172
packages/noodl-runtime/src/nodes/std-library/data/dbconfig.js
Normal file
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
const ConfigService = require('../../../api/configservice');
|
||||
|
||||
var ConfigNodeDefinition = {
|
||||
name: 'DbConfig',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/config',
|
||||
displayNodeName: 'Config',
|
||||
category: 'Cloud Services',
|
||||
usePortAsLabel: 'configKey',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
ConfigService.instance.getConfig().then((config) => {
|
||||
internal.config = config;
|
||||
if (this.hasOutput('value')) this.flagOutputDirty('value');
|
||||
});
|
||||
},
|
||||
getInspectInfo() {
|
||||
const value = this.getValue();
|
||||
|
||||
if (value === undefined) return '[No Value]';
|
||||
|
||||
return [{ type: 'value', value: value }];
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
methods: {
|
||||
getValue: function () {
|
||||
const internal = this._internal;
|
||||
if (internal.useDevValue && this.context.editorConnection && this.context.editorConnection.isRunningLocally()) {
|
||||
return internal.devValue;
|
||||
} else if (internal.config !== undefined && internal.configKey !== undefined) {
|
||||
return internal.config[internal.configKey];
|
||||
}
|
||||
},
|
||||
setInternal: function (key, value) {
|
||||
this._internal[key] = value;
|
||||
if (this.hasOutput('value')) this.flagOutputDirty('value');
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'value')
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getValue.bind(this)
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'configKey' || name === 'useDevValue' || name === 'devValue')
|
||||
return this.registerInput(name, {
|
||||
set: this.setInternal.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ConfigNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function updatePorts(node) {
|
||||
var ports = [];
|
||||
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'dbconfig-warning');
|
||||
|
||||
const configSchema = graphModel.getMetaData('dbConfigSchema');
|
||||
let valueType;
|
||||
|
||||
if (configSchema) {
|
||||
const isCloud = typeof _noodl_cloud_runtime_version !== 'undefined';
|
||||
ports.push({
|
||||
name: 'configKey',
|
||||
displayName: 'Parameter',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: Object.keys(configSchema)
|
||||
.filter((k) => isCloud || !configSchema[k].masterKeyOnly)
|
||||
.map((k) => ({ value: k, label: k })),
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (node.parameters['configKey'] !== undefined && configSchema && configSchema[node.parameters['configKey']]) {
|
||||
valueType = configSchema[node.parameters['configKey']].type;
|
||||
|
||||
if (
|
||||
valueType === 'string' ||
|
||||
valueType === 'boolean' ||
|
||||
valueType === 'number' ||
|
||||
valueType === 'object' ||
|
||||
valueType === 'array'
|
||||
) {
|
||||
ports.push({
|
||||
name: 'useDevValue',
|
||||
displayName: 'Enable',
|
||||
group: 'Local Override',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (node.parameters['useDevValue'] === true) {
|
||||
ports.push({
|
||||
name: 'devValue',
|
||||
displayName: 'Value',
|
||||
group: 'Local Override',
|
||||
type: valueType,
|
||||
plug: 'input'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (node.parameters['configKey'] !== undefined) {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'dbconfig-warning', {
|
||||
showGlobally: true,
|
||||
message: node.parameters['configKey'] + ' config parameter is missing, add it to your cloud service.'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'dbconfig-warning', {
|
||||
showGlobally: true,
|
||||
message: 'You need an active cloud service.'
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
name: 'value',
|
||||
displayName: 'Value',
|
||||
group: 'General',
|
||||
type: valueType || '*',
|
||||
plug: 'output'
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbConfigSchema', function (data) {
|
||||
ConfigService.instance.clearCache();
|
||||
updatePorts(node);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.DbConfig', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('DbConfig')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,603 @@
|
||||
'use strict';
|
||||
|
||||
const Model = require('../../../model');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
function _addBaseInfo(def, opts) {
|
||||
const _includeInputProperties = opts === undefined || opts.includeInputProperties;
|
||||
const _includeRelations = opts !== undefined && opts.includeRelations;
|
||||
|
||||
Object.assign(def.node, {
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
// Outputs
|
||||
Object.assign(def.node.outputs, {
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
scheduleOnce: function (type, cb) {
|
||||
const _this = this;
|
||||
const _type = 'hasScheduled' + type;
|
||||
if (this._internal[_type]) return;
|
||||
this._internal[_type] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this._internal[_type] = false;
|
||||
cb();
|
||||
});
|
||||
},
|
||||
checkWarningsBeforeCloudOp() {
|
||||
//clear all errors first
|
||||
this.clearWarnings();
|
||||
|
||||
if (!this._internal.collectionId) {
|
||||
this.setError('No class name specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Setup
|
||||
Object.assign(def, {
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const dbCollections = graphModel.getMetaData('dbCollections');
|
||||
const systemCollections = graphModel.getMetaData('systemCollections');
|
||||
|
||||
const _systemClasses = [
|
||||
{ label: 'User', value: '_User' },
|
||||
{ label: 'Role', value: '_Role' }
|
||||
];
|
||||
|
||||
const parameters = node.parameters;
|
||||
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
displayName: 'Class',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _systemClasses.concat(
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: []
|
||||
),
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (_includeRelations && parameters.collectionName && dbCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections)
|
||||
c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
const props = c.schema.properties;
|
||||
const enums = Object.keys(props)
|
||||
.filter((key) => props[key].type === 'Relation')
|
||||
.map((key) => ({ label: key, value: key }));
|
||||
|
||||
ports.push({
|
||||
name: 'relationProperty',
|
||||
displayName: 'Relation',
|
||||
group: 'General',
|
||||
type: { name: 'enum', enums: enums, allowEditOnly: true },
|
||||
plug: 'input'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_includeInputProperties && parameters.collectionName && dbCollections) {
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
// Fetch ports from collection keys
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections)
|
||||
c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
for (var key in props) {
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def._additionalDynamicPorts && def._additionalDynamicPorts(node, ports, graphModel);
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
_updatePorts();
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
_updatePorts();
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.' + def.node.name, function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType(def.node.name)) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _addModelId(def, opts) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
const _includeInputs = opts === undefined || opts.includeInputs;
|
||||
const _includeOutputs = opts === undefined || opts.includeOutputs;
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
if (_includeInputs) {
|
||||
Object.assign(def.node, {
|
||||
usePortAsLabel: 'collectionName'
|
||||
});
|
||||
|
||||
def.node.dynamicports = (def.node.dynamicports || []).concat([
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
]);
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Id Source',
|
||||
group: 'General',
|
||||
tooltip:
|
||||
'Choose if you want to specify the Id explicitly, \n or if you want it to be that of the current record in a repeater.',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'ModelName',
|
||||
identifierDisplayName: 'Object Ids'
|
||||
},
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
this.setModelID(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Outputs
|
||||
if (_includeOutputs) {
|
||||
Object.assign(def.node.outputs, {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
setCollectionID: function (id) {
|
||||
this._internal.collectionId = id;
|
||||
this.clearWarnings();
|
||||
},
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
this.setModel(model);
|
||||
},
|
||||
setModel: function (model) {
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'collectionName')
|
||||
this.registerInput(name, {
|
||||
set: this.setCollectionID.bind(this)
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _addInputProperties(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
Object.assign(def.node, {
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
|
||||
_def.node.initialize && _def.node.initialize.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Outputs
|
||||
Object.assign(def.node.outputs, {});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: this._setInputValue.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
_setInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _addRelationProperty(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
targetId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Target Record Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.targetModelId = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'relationProperty')
|
||||
this.registerInput(name, {
|
||||
set: this.setRelationProperty.bind(this)
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
setRelationProperty: function (value) {
|
||||
this._internal.relationProperty = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _getCurrentUser(modelScope) {
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// We are running in browser, try to find the current user
|
||||
|
||||
var _cu = localStorage['Parse/' + CloudStore.instance.appId + '/currentUser'];
|
||||
if (_cu !== undefined) {
|
||||
let cu;
|
||||
try {
|
||||
cu = JSON.parse(_cu);
|
||||
} catch (e) {}
|
||||
|
||||
return cu !== undefined ? cu.objectId : undefined;
|
||||
}
|
||||
} else {
|
||||
// Assume we are running in cloud runtime
|
||||
const request = modelScope.get('Request');
|
||||
return request.UserId;
|
||||
}
|
||||
}
|
||||
|
||||
function _addAccessControl(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
Object.assign(def.node, {
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.accessControl = {};
|
||||
|
||||
_def.node.initialize && _def.node.initialize.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
accessControl: {
|
||||
type: { name: 'proplist', autoName: 'Rule', allowEditOnly: true },
|
||||
index: 1000,
|
||||
displayName: 'Access Control Rules',
|
||||
group: 'Access Control Rules',
|
||||
set: function (value) {
|
||||
this._internal.accessControlRules = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Dynamic ports
|
||||
const _super = def._additionalDynamicPorts;
|
||||
def._additionalDynamicPorts = function (node, ports, graphModel) {
|
||||
if (node.parameters['accessControl'] !== undefined && node.parameters['accessControl'].length > 0) {
|
||||
node.parameters['accessControl'].forEach((ac) => {
|
||||
const prefix = 'acl-' + ac.id;
|
||||
// User or role?
|
||||
ports.push({
|
||||
name: prefix + '-target',
|
||||
displayName: 'Target',
|
||||
editorName: ac.label + ' | Target',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'everyone', label: 'Everyone' },
|
||||
{ value: 'role', label: 'Role' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: ac.label + ' Access Rule',
|
||||
default: 'user',
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
|
||||
if (node.parameters[prefix + '-target'] === 'role') {
|
||||
ports.push({
|
||||
name: prefix + '-role',
|
||||
displayName: 'Role',
|
||||
editorName: ac.label + ' | Role',
|
||||
group: ac.label + ' Access Rule',
|
||||
plug: 'input',
|
||||
type: 'string',
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
} else if (
|
||||
node.parameters[prefix + '-target'] === undefined ||
|
||||
node.parameters[prefix + '-target'] === 'user'
|
||||
) {
|
||||
ports.push({
|
||||
name: prefix + '-userid',
|
||||
displayName: 'User Id',
|
||||
group: ac.label + ' Access Rule',
|
||||
editorName: ac.label + ' | User Id',
|
||||
plug: 'input',
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
}
|
||||
|
||||
// Read
|
||||
ports.push({
|
||||
name: prefix + '-read',
|
||||
displayName: 'Read',
|
||||
editorName: ac.label + ' | Read',
|
||||
group: ac.label + ' Access Rule',
|
||||
plug: 'input',
|
||||
type: { name: 'boolean' },
|
||||
default: true,
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
|
||||
// Write
|
||||
ports.push({
|
||||
name: prefix + '-write',
|
||||
displayName: 'Write',
|
||||
editorName: ac.label + ' | Write',
|
||||
group: ac.label + ' Access Rule',
|
||||
plug: 'input',
|
||||
type: { name: 'boolean' },
|
||||
default: true,
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_super && _super(node, ports, graphModel);
|
||||
};
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('acl-'))
|
||||
this.registerInput(name, {
|
||||
set: this.setAccessControl.bind(this, name)
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
_getACL: function () {
|
||||
let acl = {};
|
||||
|
||||
function _rule(rule) {
|
||||
return {
|
||||
read: rule.read === undefined ? true : rule.read,
|
||||
write: rule.write === undefined ? true : rule.write
|
||||
};
|
||||
}
|
||||
|
||||
const currentUserId = _getCurrentUser(this.nodeScope.modelScope);
|
||||
|
||||
if (this._internal.accessControlRules !== undefined) {
|
||||
this._internal.accessControlRules.forEach((r) => {
|
||||
const rule = this._internal.accessControl[r.id];
|
||||
|
||||
if (rule === undefined) {
|
||||
const userId = currentUserId;
|
||||
if (userId !== undefined) acl[userId] = { write: true, read: true };
|
||||
} else if (rule.target === 'everyone') {
|
||||
acl['*'] = _rule(rule);
|
||||
} else if (rule.target === 'user') {
|
||||
const userId = rule.userid || currentUserId;
|
||||
acl[userId] = _rule(rule);
|
||||
} else if (rule.target === 'role') {
|
||||
acl['role:' + rule.role] = _rule(rule);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(acl).length > 0 ? acl : undefined;
|
||||
},
|
||||
setAccessControl: function (name, value) {
|
||||
const _parts = name.split('-');
|
||||
|
||||
if (this._internal.accessControl[_parts[1]] === undefined) this._internal.accessControl[_parts[1]] = {};
|
||||
this._internal.accessControl[_parts[1]][_parts[2]] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addInputProperties: _addInputProperties,
|
||||
addModelId: _addModelId,
|
||||
addBaseInfo: _addBaseInfo,
|
||||
addRelationProperty: _addRelationProperty,
|
||||
addAccessControl: _addAccessControl
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var AddDbModelRelationNodeDefinition = {
|
||||
node: {
|
||||
name: 'AddDbModelRelation',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/add-record-relation',
|
||||
displayNodeName: 'Add Record Relation',
|
||||
usePortAsLabel: 'collectionName',
|
||||
// shortDesc: "Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.",
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleAddRelation();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
relationAdded: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateInputs: function () {
|
||||
if (!this.context.editorConnection) return;
|
||||
|
||||
const _warning = (message) => {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation', {
|
||||
message
|
||||
});
|
||||
};
|
||||
|
||||
if (this._internal.collectionId === undefined) {
|
||||
_warning('No class specified');
|
||||
} else if (this._internal.relationProperty === undefined) {
|
||||
_warning('No relation property specified');
|
||||
} else if (this._internal.targetModelId === undefined) {
|
||||
_warning('No target record Id (the record to add a relation to) specified');
|
||||
} else if (this._internal.model === undefined) {
|
||||
_warning('No record Id specified (the record that should get the relation)');
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation');
|
||||
}
|
||||
},
|
||||
scheduleAddRelation: function (key) {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('StorageAddRelation', function () {
|
||||
_this.validateInputs();
|
||||
|
||||
if (!internal.model) return;
|
||||
var model = internal.model;
|
||||
|
||||
var targetModelId = internal.targetModelId;
|
||||
if (targetModelId === undefined) return;
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.modelScope).addRelation({
|
||||
collection: internal.collectionId,
|
||||
objectId: model.getId(),
|
||||
key: internal.relationProperty,
|
||||
targetObjectId: targetModelId,
|
||||
targetClass: (_this.nodeScope.modelScope || Model).get(targetModelId)._class,
|
||||
success: function (response) {
|
||||
for (var _key in response) {
|
||||
model.set(_key, response[_key]);
|
||||
}
|
||||
|
||||
// Successfully added relation
|
||||
_this.sendSignalOnOutput('relationAdded');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to add relation.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(AddDbModelRelationNodeDefinition, {
|
||||
includeRelations: true
|
||||
});
|
||||
DbModelCRUDBase.addModelId(AddDbModelRelationNodeDefinition);
|
||||
DbModelCRUDBase.addRelationProperty(AddDbModelRelationNodeDefinition);
|
||||
|
||||
module.exports = AddDbModelRelationNodeDefinition;
|
||||
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var AddDbModelRelationNodeDefinition = {
|
||||
node: {
|
||||
name: 'RemoveDbModelRelation',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/remove-record-relation',
|
||||
displayName: 'Remove Record Relation',
|
||||
usePortAsLabel: 'collectionName',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRemoveRelation();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
relationRemoved: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateInputs: function () {
|
||||
if (!this.context.editorConnection) return;
|
||||
|
||||
const _warning = (message) => {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation', {
|
||||
message
|
||||
});
|
||||
};
|
||||
|
||||
if (this._internal.collectionId === undefined) {
|
||||
_warning('No class specified');
|
||||
} else if (this._internal.relationProperty === undefined) {
|
||||
_warning('No relation property specified');
|
||||
} else if (this._internal.targetModelId === undefined) {
|
||||
_warning('No target record Id (the record to add a relation to) specified');
|
||||
} else if (this._internal.model === undefined) {
|
||||
_warning('No record Id specified (the record that should get the relation)');
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation');
|
||||
}
|
||||
},
|
||||
scheduleRemoveRelation: function (key) {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('StorageRemoveRelation', function () {
|
||||
_this.validateInputs();
|
||||
|
||||
if (!internal.model) return;
|
||||
var model = internal.model;
|
||||
|
||||
var targetModelId = internal.targetModelId;
|
||||
if (targetModelId === undefined) return;
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.modelScope).removeRelation({
|
||||
collection: internal.collectionId,
|
||||
objectId: model.getId(),
|
||||
key: internal.relationProperty,
|
||||
targetObjectId: targetModelId,
|
||||
targetClass: (_this.nodeScope.modelScope || Model).get(targetModelId)._class,
|
||||
success: function (response) {
|
||||
for (var _key in response) {
|
||||
model.set(_key, response[_key]);
|
||||
}
|
||||
|
||||
// Successfully removed relation
|
||||
_this.sendSignalOnOutput('relationRemoved');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to remove relation.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(AddDbModelRelationNodeDefinition, {
|
||||
includeRelations: true
|
||||
});
|
||||
DbModelCRUDBase.addModelId(AddDbModelRelationNodeDefinition);
|
||||
DbModelCRUDBase.addRelationProperty(AddDbModelRelationNodeDefinition);
|
||||
|
||||
module.exports = AddDbModelRelationNodeDefinition;
|
||||
@@ -0,0 +1,401 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');
|
||||
|
||||
var Model = require('../../../model');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var ModelNodeDefinition = {
|
||||
name: 'DbModel2',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/record',
|
||||
displayNodeName: 'Record',
|
||||
shortDesc: 'Database model',
|
||||
category: 'Cloud Services',
|
||||
usePortAsLabel: 'collectionName',
|
||||
color: 'data',
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
],
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
internal.relationModelIds = {};
|
||||
|
||||
var _this = this;
|
||||
this._internal.onModelChangedCallback = function (args) {
|
||||
if (_this.isInputConnected('fetch')) return;
|
||||
|
||||
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
|
||||
|
||||
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
|
||||
|
||||
_this.sendSignalOnOutput('changed');
|
||||
};
|
||||
},
|
||||
getInspectInfo() {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Record]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: model.data }
|
||||
];
|
||||
},
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
group: 'Events'
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Id Source',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId();
|
||||
// Can be passed as model as well
|
||||
else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it
|
||||
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
if (this.isInputConnected('fetch') === false) this.setModelID(value);
|
||||
else {
|
||||
this.flagOutputDirty('id');
|
||||
}
|
||||
}
|
||||
},
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCollectionID: function (id) {
|
||||
this._internal.collectionId = id;
|
||||
},
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
// this._internal.modelIsNew = false;
|
||||
this.setModel(model);
|
||||
},
|
||||
setModel: function (model) {
|
||||
if (this._internal.model)
|
||||
// Remove old listener if existing
|
||||
this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
model.on('change', this._internal.onModelChangedCallback);
|
||||
|
||||
// We have a new model, mark all outputs as dirty
|
||||
for (var key in model.data) {
|
||||
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
},
|
||||
scheduleOnce: function (type, cb) {
|
||||
const _this = this;
|
||||
const _type = 'hasScheduled' + type;
|
||||
if (this._internal[_type]) return;
|
||||
this._internal[_type] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this._internal[_type] = false;
|
||||
cb();
|
||||
});
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
|
||||
}
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
var _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('Fetch', function () {
|
||||
// Don't do fetch if no id
|
||||
if (internal.modelId === undefined || internal.modelId === '') {
|
||||
_this.setError('Missing Id.');
|
||||
return;
|
||||
}
|
||||
|
||||
const cloudstore = CloudStore.forScope(_this.nodeScope.modelScope);
|
||||
cloudstore.fetch({
|
||||
collection: internal.collectionId,
|
||||
objectId: internal.modelId, // Get the objectId part of the model id
|
||||
success: function (response) {
|
||||
var model = cloudstore._fromJSON(response, internal.collectionId);
|
||||
if (internal.model !== model) {
|
||||
// Check if we need to change model
|
||||
if (internal.model)
|
||||
// Remove old listener if existing
|
||||
internal.model.off('change', internal.onModelChangedCallback);
|
||||
|
||||
internal.model = model;
|
||||
model.on('change', internal.onModelChangedCallback);
|
||||
}
|
||||
_this.flagOutputDirty('id');
|
||||
|
||||
delete response.objectId;
|
||||
|
||||
for (var key in response) {
|
||||
if (_this.hasOutput('prop-' + key)) _this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
|
||||
_this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to fetch.');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
scheduleStore: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
if (!internal.model) return;
|
||||
|
||||
this.scheduleOnce('Store', function () {
|
||||
for (var i in internal.inputValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerOutput(name, {
|
||||
getter: userOutputGetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicSignals = {};
|
||||
|
||||
if (dynamicSignals[name])
|
||||
return this.registerInput(name, {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: dynamicSignals[name]
|
||||
})
|
||||
});
|
||||
|
||||
const dynamicSetters = {
|
||||
collectionName: this.setCollectionID.bind(this)
|
||||
};
|
||||
|
||||
if (dynamicSetters[name])
|
||||
return this.registerInput(name, {
|
||||
set: dynamicSetters[name]
|
||||
});
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userOutputGetter(name) {
|
||||
/* jshint validthis:true */
|
||||
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
|
||||
}
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
//console.log('dbmodel setter:',name,value)
|
||||
/* jshint validthis:true */
|
||||
this._internal.inputValues[name] = value;
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, graphModel) {
|
||||
var ports = [];
|
||||
|
||||
const dbCollections = graphModel.getMetaData('dbCollections');
|
||||
const systemCollections = graphModel.getMetaData('systemCollections');
|
||||
|
||||
const _systemClasses = [
|
||||
{ label: 'User', value: '_User' },
|
||||
{ label: 'Role', value: '_Role' }
|
||||
];
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
displayName: 'Class',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _systemClasses.concat(
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: []
|
||||
),
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (parameters.collectionName && dbCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections) c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
for (var key in props) {
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
if (p.type === 'Relation') {
|
||||
} else {
|
||||
// Other schema type ports
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: key + ' Changed',
|
||||
name: 'changed-' + key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: ModelNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.DbModel2', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('DbModel2')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var DeleteDbModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'DeleteDbModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/delete-record',
|
||||
displayNodeName: 'Delete Record',
|
||||
shortDesc:
|
||||
'Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.storageDelete();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
storageDelete: function () {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
if (!this.checkWarningsBeforeCloudOp()) return;
|
||||
|
||||
this.scheduleOnce('StorageDelete', function () {
|
||||
if (!internal.model) {
|
||||
_this.setError('Missing Record Id');
|
||||
return;
|
||||
}
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.ModelScope).delete({
|
||||
collection: internal.collectionId,
|
||||
objectId: internal.model.getId(), // Get the objectId part of the model id,
|
||||
success: function () {
|
||||
internal.model.notify('delete'); // Notify that this model has been deleted
|
||||
_this.sendSignalOnOutput('deleted');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to delete.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(DeleteDbModelPropertiedNodeDefinition, {
|
||||
includeInputProperties: false
|
||||
});
|
||||
DbModelCRUDBase.addModelId(DeleteDbModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = DeleteDbModelPropertiedNodeDefinition;
|
||||
@@ -0,0 +1,519 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('../../../../noodl-runtime');
|
||||
|
||||
const Collection = require('../../../collection'),
|
||||
Model = require('../../../model'),
|
||||
CloudStore = require('../../../api/cloudstore'),
|
||||
QueryUtils = require('../../../api/queryutils');
|
||||
|
||||
var FilterDBModelsNode = {
|
||||
name: 'FilterDBModels',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/filter-records',
|
||||
displayNodeName: 'Filter Records',
|
||||
shortDesc: 'Filter, sort and limit array',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
|
||||
this._internal.collectionChangedCallback = function () {
|
||||
if (_this.isInputConnected('filter') === true) return;
|
||||
|
||||
_this.scheduleFilter();
|
||||
};
|
||||
|
||||
this._internal.cloudStoreEvents = function (args) {
|
||||
if (_this.isInputConnected('filter') === true) return;
|
||||
|
||||
if (_this._internal.visualFilter === undefined) return;
|
||||
if (_this._internal.collection === undefined) return;
|
||||
if (args.collection !== _this._internal.collectionName) return;
|
||||
|
||||
if (args.objectId !== undefined && _this._internal.collection.contains(Model.get(args.objectId)))
|
||||
_this.scheduleFilter();
|
||||
};
|
||||
|
||||
CloudStore.instance.on('save', this._internal.cloudStoreEvents);
|
||||
|
||||
this._internal.enabled = true;
|
||||
this._internal.filterSettings = {};
|
||||
this._internal.filterParameters = {};
|
||||
// this._internal.filteredCollection = Collection.get();
|
||||
},
|
||||
getInspectInfo() {
|
||||
const collection = this._internal.filteredCollection;
|
||||
|
||||
if (!collection) {
|
||||
return { type: 'text', value: '[Not executed yet]' };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Id: ' + collection.getId()
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
value: collection.items
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
this.bindCollection(value);
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
group: 'General',
|
||||
displayName: 'Enabled',
|
||||
default: true,
|
||||
set: function (value) {
|
||||
this._internal.enabled = value;
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
type: 'signal',
|
||||
group: 'Actions',
|
||||
displayName: 'Filter',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFilter();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.filteredCollection;
|
||||
}
|
||||
},
|
||||
firstItemId: {
|
||||
type: 'string',
|
||||
displayName: 'First Record Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if (this._internal.filteredCollection !== undefined) {
|
||||
const firstItem = this._internal.filteredCollection.get(0);
|
||||
if (firstItem !== undefined) return firstItem.getId();
|
||||
}
|
||||
}
|
||||
},
|
||||
/* firstItem:{
|
||||
type: 'object',
|
||||
displayName: 'First Item',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if(this._internal.filteredCollection !== undefined) {
|
||||
return this._internal.filteredCollection.get(0);
|
||||
}
|
||||
}
|
||||
}, */
|
||||
count: {
|
||||
type: 'number',
|
||||
displayName: 'Count',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.filteredCollection ? this._internal.filteredCollection.size() : 0;
|
||||
}
|
||||
},
|
||||
modified: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Filtered'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
unbindCurrentCollection: function () {
|
||||
var collection = this._internal.collection;
|
||||
if (!collection) return;
|
||||
collection.off('change', this._internal.collectionChangedCallback);
|
||||
this._internal.collection = undefined;
|
||||
},
|
||||
bindCollection: function (collection) {
|
||||
this.unbindCurrentCollection();
|
||||
this._internal.collection = collection;
|
||||
collection && collection.on('change', this._internal.collectionChangedCallback);
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this.unbindCurrentCollection();
|
||||
|
||||
CloudStore.instance.off('save', this._internal.cloudStoreEvents);
|
||||
},
|
||||
/* getFilter: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
const options = ['case'] // List all supported options here
|
||||
|
||||
if (filterSettings['filterFilter']) {
|
||||
const filters = filterSettings['filterFilter'].split(',');
|
||||
var _filter = {};
|
||||
filters.forEach(function (f) {
|
||||
var op = '$' + (filterSettings['filterFilterOp-' + f] || 'eq');
|
||||
_filter[f] = {};
|
||||
_filter[f][op] = filterSettings['filterFilterValue-' + f];
|
||||
|
||||
options.forEach((o) => {
|
||||
var option = filterSettings['filterFilterOption-' + o + '-' + f];
|
||||
if(option) _filter[f]['$' + o] = option
|
||||
})
|
||||
})
|
||||
return _filter;
|
||||
}
|
||||
},
|
||||
getSort: function() {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (filterSettings['filterSort']) {
|
||||
const sort = filterSettings['filterSort'].split(',');
|
||||
var _sort = {};
|
||||
sort.forEach(function (s) {
|
||||
_sort[s] = filterSettings['filterSort-'+s] === 'descending'?-1:1;
|
||||
})
|
||||
return _sort;
|
||||
}
|
||||
},*/
|
||||
getLimit: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (!filterSettings['filterEnableLimit']) return;
|
||||
else return filterSettings['filterLimit'] || 10;
|
||||
},
|
||||
getSkip: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (!filterSettings['filterEnableLimit']) return;
|
||||
else return filterSettings['filterSkip'] || 0;
|
||||
},
|
||||
scheduleFilter: function () {
|
||||
if (this.collectionChangedScheduled) return;
|
||||
this.collectionChangedScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.collectionChangedScheduled = false;
|
||||
if (!this._internal.collection) return;
|
||||
|
||||
// Apply filter and write to output collection
|
||||
var filtered = [].concat(this._internal.collection.items);
|
||||
|
||||
if (this._internal.enabled) {
|
||||
const _filter = this._internal.visualFilter;
|
||||
if (_filter !== undefined) {
|
||||
var filter = QueryUtils.convertVisualFilter(_filter, {
|
||||
queryParameters: this._internal.filterParameters,
|
||||
collectionName: this._internal.collectionName
|
||||
});
|
||||
if (filter) filtered = filtered.filter((m) => QueryUtils.matchesQuery(m, filter));
|
||||
}
|
||||
|
||||
var _sort = this._internal.visualSorting;
|
||||
if (_sort !== undefined && _sort.length > 0) {
|
||||
var sort = QueryUtils.convertVisualSorting(_sort);
|
||||
}
|
||||
if (sort) filtered.sort(QueryUtils.compareObjects.bind(this, sort));
|
||||
|
||||
var skip = this.getSkip();
|
||||
if (skip) filtered = filtered.slice(skip, filtered.length);
|
||||
|
||||
var limit = this.getLimit();
|
||||
if (limit) filtered = filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
this._internal.filteredCollection = Collection.create(filtered);
|
||||
|
||||
this.sendSignalOnOutput('modified');
|
||||
this.flagOutputDirty('firstItemId');
|
||||
this.flagOutputDirty('items');
|
||||
this.flagOutputDirty('count');
|
||||
});
|
||||
},
|
||||
setCollectionName: function (name) {
|
||||
this._internal.collectionName = name;
|
||||
},
|
||||
setVisualFilter: function (value) {
|
||||
this._internal.visualFilter = value;
|
||||
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
},
|
||||
setVisualSorting: function (value) {
|
||||
this._internal.visualSorting = value;
|
||||
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
},
|
||||
setFilterParameter: function (name, value) {
|
||||
this._internal.filterParameters[name] = value;
|
||||
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'collectionName')
|
||||
return this.registerInput(name, {
|
||||
set: this.setCollectionName.bind(this)
|
||||
});
|
||||
|
||||
if (name === 'visualFilter')
|
||||
return this.registerInput(name, {
|
||||
set: this.setVisualFilter.bind(this)
|
||||
});
|
||||
|
||||
if (name === 'visualSorting')
|
||||
return this.registerInput(name, {
|
||||
set: this.setVisualSorting.bind(this)
|
||||
});
|
||||
|
||||
if (name.startsWith('fp-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setFilterParameter.bind(this, name.substring('fp-'.length))
|
||||
});
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
this._internal.filterSettings[name] = value;
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, dbCollections) {
|
||||
var ports = [];
|
||||
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums:
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: [],
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Class',
|
||||
plug: 'input',
|
||||
group: 'General'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterEnableLimit',
|
||||
displayName: 'Use limit'
|
||||
});
|
||||
|
||||
if (parameters['filterEnableLimit']) {
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 10,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterLimit',
|
||||
displayName: 'Limit'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 0,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterSkip',
|
||||
displayName: 'Skip'
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters.collectionName !== undefined) {
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
const schema = JSON.parse(JSON.stringify(c.schema));
|
||||
|
||||
const _supportedTypes = {
|
||||
Boolean: true,
|
||||
String: true,
|
||||
Date: true,
|
||||
Number: true,
|
||||
Pointer: true
|
||||
};
|
||||
for (var key in schema.properties) {
|
||||
if (!_supportedTypes[schema.properties[key].type]) delete schema.properties[key];
|
||||
}
|
||||
|
||||
ports.push({
|
||||
name: 'visualFilter',
|
||||
plug: 'input',
|
||||
type: { name: 'query-filter', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Filter',
|
||||
group: 'Filter'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'visualSorting',
|
||||
plug: 'input',
|
||||
type: { name: 'query-sorting', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Sorting',
|
||||
group: 'Sorting'
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters.visualFilter !== undefined) {
|
||||
// Find all input ports
|
||||
const uniqueInputs = {};
|
||||
function _collectInputs(query) {
|
||||
if (query === undefined) return;
|
||||
if (query.rules !== undefined) query.rules.forEach((r) => _collectInputs(r));
|
||||
else if (query.input !== undefined) uniqueInputs[query.input] = true;
|
||||
}
|
||||
|
||||
_collectInputs(parameters.visualFilter);
|
||||
Object.keys(uniqueInputs).forEach((input) => {
|
||||
ports.push({
|
||||
name: 'fp-' + input,
|
||||
plug: 'input',
|
||||
type: '*',
|
||||
displayName: input,
|
||||
group: 'Filter Parameters'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ports.push({
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
plug: 'input',
|
||||
group: 'Filter',
|
||||
name: 'filterFilter',
|
||||
displayName: 'Filter',
|
||||
})
|
||||
|
||||
ports.push({
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
plug: 'input',
|
||||
group: 'Sort',
|
||||
name: 'filterSort',
|
||||
displayName: 'Sort',
|
||||
})
|
||||
|
||||
const filterOps = {
|
||||
"string": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' },{value: 'regex', label: 'Matches RegEx'}],
|
||||
"boolean": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' }],
|
||||
"number": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' }, { value: 'lt', label: 'Less than' }, { value: 'gt', label: 'Greater than' },
|
||||
{ value: 'gte', label: 'Greater than or equal' }, { value: 'lte', label: 'Less than or equal' }]
|
||||
}
|
||||
|
||||
if (parameters['filterFilter']) {
|
||||
var filters = parameters['filterFilter'].split(',');
|
||||
filters.forEach((f) => {
|
||||
// Type
|
||||
ports.push({
|
||||
type: { name: 'enum', enums: [{ value: 'string', label: 'String' }, { value: 'number', label: 'Number' }, { value: 'boolean', label: 'Boolean' }] },
|
||||
default: 'string',
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Type',
|
||||
editorName: f + ' filter | Type',
|
||||
name: 'filterFilterType-' + f
|
||||
})
|
||||
|
||||
var type = parameters['filterFilterType-' + f];
|
||||
|
||||
// String filter type
|
||||
ports.push({
|
||||
type: { name: 'enum', enums: filterOps[type || 'string'] },
|
||||
default: 'eq',
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Op',
|
||||
editorName: f + ' filter| Op',
|
||||
name: 'filterFilterOp-' + f
|
||||
})
|
||||
|
||||
// Case sensitivite option
|
||||
if(parameters['filterFilterOp-' + f] === 'regex') {
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Case sensitive',
|
||||
editorName: f + ' filter| Case',
|
||||
name: 'filterFilterOption-case-' + f
|
||||
})
|
||||
}
|
||||
|
||||
ports.push({
|
||||
type: type || 'string',
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Value',
|
||||
editorName: f + ' Filter Value',
|
||||
name: 'filterFilterValue-' + f
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
if (parameters['filterSort']) {
|
||||
var filters = parameters['filterSort'].split(',');
|
||||
filters.forEach((f) => {
|
||||
ports.push({
|
||||
type: { name: 'enum', enums: [{ value: 'ascending', label: 'Ascending' }, { value: 'descending', label: 'Descending' }] },
|
||||
default: 'ascending',
|
||||
plug: 'input',
|
||||
group: f + ' sort',
|
||||
displayName: 'Sort',
|
||||
editorName: f + ' sorting',
|
||||
name: 'filterSort-' + f
|
||||
})
|
||||
})
|
||||
}*/
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: FilterDBModelsNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.FilterDBModels', function (node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,342 @@
|
||||
'use strict';
|
||||
|
||||
const Collection = require('../../../collection');
|
||||
const Model = require('../../../model');
|
||||
|
||||
function _addBaseInfo(def) {
|
||||
Object.assign(def.node, {
|
||||
category: 'Data',
|
||||
color: 'data'
|
||||
});
|
||||
}
|
||||
|
||||
function _addModelId(def, opts) {
|
||||
const _includeInputs = opts === undefined || opts.includeInputs;
|
||||
const _includeOutputs = opts === undefined || opts.includeOutputs;
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
if (_includeInputs) {
|
||||
Object.assign(def.node, {
|
||||
usePortAsLabel: 'modelId'
|
||||
});
|
||||
|
||||
def.node.dynamicports = (def.node.dynamicports || []).concat([
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
]);
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Id Source',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'ModelName',
|
||||
identifierDisplayName: 'Object Ids'
|
||||
},
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
this.setModelID(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Outputs
|
||||
if (_includeOutputs) {
|
||||
Object.assign(def.node.outputs, {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
this.setModel(model);
|
||||
},
|
||||
setModel: function (model) {
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
}
|
||||
});
|
||||
|
||||
//Inspect model
|
||||
if (!def.node.getInspectInfo) {
|
||||
def.node.getInspectInfo = function () {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Object]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: model.data }
|
||||
];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function _addInputProperties(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
Object.assign(def, {
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.' + def.node.name, function (node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const _types = [
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'Object', value: 'object' },
|
||||
{ label: 'Any', value: '*' }
|
||||
];
|
||||
|
||||
// Add value outputs
|
||||
var properties = node.parameters.properties;
|
||||
if (properties) {
|
||||
properties = properties ? properties.split(',') : undefined;
|
||||
for (var i in properties) {
|
||||
var p = properties[i];
|
||||
|
||||
// Property input
|
||||
ports.push({
|
||||
type: {
|
||||
name: node.parameters['type-' + p] === undefined ? '*' : node.parameters['type-' + p]
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Property Values',
|
||||
displayName: p,
|
||||
// editorName:p,
|
||||
name: 'prop-' + p
|
||||
});
|
||||
|
||||
// Property type
|
||||
ports.push({
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _types,
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Property Types',
|
||||
displayName: p,
|
||||
default: '*',
|
||||
name: 'type-' + p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'input'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
_updatePorts();
|
||||
});
|
||||
});
|
||||
|
||||
_def.setup && _def.setup(context, graphModel);
|
||||
}
|
||||
});
|
||||
|
||||
// Initilize
|
||||
Object.assign(def.node, {
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
internal.inputTypes = {};
|
||||
|
||||
_def.node.initialize && _def.node.initialize.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Outputs
|
||||
Object.assign(def.node.outputs, {});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
properties: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
displayName: 'Properties',
|
||||
group: 'Properties to set',
|
||||
set: function (value) {}
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
_pushInputValues: function (model) {
|
||||
var internal = this._internal;
|
||||
|
||||
const _defaultValueForType = {
|
||||
boolean: false,
|
||||
string: '',
|
||||
number: 0,
|
||||
date: new Date()
|
||||
};
|
||||
|
||||
const _allKeys = {};
|
||||
for (const key in internal.inputTypes) _allKeys[key] = true;
|
||||
for (const key in internal.inputValues) _allKeys[key] = true;
|
||||
|
||||
const properties = this.model.parameters.properties || '';
|
||||
|
||||
const validProperties = properties.split(',');
|
||||
|
||||
const keysToSet = Object.keys(_allKeys).filter((key) => validProperties.indexOf(key) !== -1);
|
||||
|
||||
for (const i of keysToSet) {
|
||||
var value = internal.inputValues[i];
|
||||
|
||||
if (value !== undefined) {
|
||||
//Parse array types with string as javascript
|
||||
if (internal.inputTypes[i] !== undefined && internal.inputTypes[i] === 'array' && typeof value === 'string') {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'invalid-array-' + i
|
||||
);
|
||||
|
||||
try {
|
||||
value = eval(value); //this might be static data in the form of javascript
|
||||
} catch (e) {
|
||||
if (value.indexOf('[') !== -1 || value.indexOf('{') !== -1) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'invalid-array-' + i,
|
||||
{
|
||||
showGlobally: true,
|
||||
message: 'Invalid array<br>' + e.toString()
|
||||
}
|
||||
);
|
||||
value = [];
|
||||
} else {
|
||||
//backwards compability with how this node used to work
|
||||
value = Collection.get(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Resolve object from IDs
|
||||
if (
|
||||
internal.inputTypes[i] !== undefined &&
|
||||
internal.inputTypes[i] === 'object' &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = (this.nodeScope.modelScope || Model).get(value);
|
||||
}
|
||||
|
||||
model.set(i, value, { resolve: true });
|
||||
} else {
|
||||
model.set(i, _defaultValueForType[internal.inputTypes[i]], {
|
||||
resolve: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
scheduleStore: function () {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledStore = false;
|
||||
if (!internal.model) return;
|
||||
|
||||
this._pushInputValues(internal.model);
|
||||
|
||||
this.sendSignalOnOutput('stored');
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: this._setInputValue.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
|
||||
if (name.startsWith('type-'))
|
||||
this.registerInput(name, {
|
||||
set: this._setInputType.bind(this, name.substring('type-'.length))
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _def.node.methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
_setInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
},
|
||||
_setInputType: function (name, value) {
|
||||
this._internal.inputTypes[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addInputProperties: _addInputProperties,
|
||||
addModelId: _addModelId,
|
||||
addBaseInfo: _addBaseInfo
|
||||
};
|
||||
277
packages/noodl-runtime/src/nodes/std-library/data/modelnode2.js
Normal file
277
packages/noodl-runtime/src/nodes/std-library/data/modelnode2.js
Normal file
@@ -0,0 +1,277 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('../../../../noodl-runtime');
|
||||
|
||||
var Model = require('../../../model');
|
||||
|
||||
var ModelNodeDefinition = {
|
||||
name: 'Model2',
|
||||
docs: 'https://docs.noodl.net/nodes/data/object/object-node',
|
||||
displayNodeName: 'Object',
|
||||
shortDesc:
|
||||
'Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'modelId',
|
||||
color: 'data',
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
],
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
internal.dirtyValues = {};
|
||||
|
||||
var _this = this;
|
||||
this._internal.onModelChangedCallback = function (args) {
|
||||
if (_this.isInputConnected('fetch') === true) return;
|
||||
|
||||
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
|
||||
|
||||
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
|
||||
|
||||
_this.sendSignalOnOutput('changed');
|
||||
};
|
||||
},
|
||||
getInspectInfo() {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Object]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: model.data }
|
||||
];
|
||||
},
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Get Id from',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'ModelName',
|
||||
identifierDisplayName: 'Object Ids'
|
||||
},
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId();
|
||||
// Can be passed as model as well
|
||||
else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it
|
||||
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
if (this.isInputConnected('fetch') === false) this.setModelID(value);
|
||||
else {
|
||||
this.flagOutputDirty('id');
|
||||
}
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
displayName: 'Properties',
|
||||
group: 'Properties',
|
||||
set: function (value) {}
|
||||
},
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleSetModel();
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
scheduleStore: function () {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledStore = false;
|
||||
if (!internal.model) return;
|
||||
|
||||
for (var i in internal.dirtyValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
internal.dirtyValues = {}; // Reset dirty values
|
||||
});
|
||||
},
|
||||
scheduleSetModel: function () {
|
||||
if (this.hasScheduledSetModel) return;
|
||||
this.hasScheduledSetModel = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledSetModel = false;
|
||||
this.setModelID(this._internal.modelId);
|
||||
});
|
||||
},
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
this.setModel(model);
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
setModel: function (model) {
|
||||
if (this._internal.model)
|
||||
// Remove old listener if existing
|
||||
this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
|
||||
// In set idSource, we are calling setModel with undefined
|
||||
if (model) {
|
||||
model.on('change', this._internal.onModelChangedCallback);
|
||||
|
||||
// We have a new model, mark all outputs as dirty
|
||||
for (var key in model.data) {
|
||||
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
}
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerOutput(name, {
|
||||
getter: userOutputGetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userOutputGetter(name) {
|
||||
/* jshint validthis:true */
|
||||
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
|
||||
}
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
this._internal.inputValues[name] = value;
|
||||
|
||||
// Store on change if no connection to store or new
|
||||
const model = this._internal.model;
|
||||
const valueChanged = model ? model.get(name) !== value : true;
|
||||
if (valueChanged) {
|
||||
this._internal.dirtyValues[name] = true;
|
||||
this.scheduleStore();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
var ports = [];
|
||||
|
||||
// Add value outputs
|
||||
var properties = parameters.properties;
|
||||
if (properties) {
|
||||
properties = properties ? properties.split(',') : undefined;
|
||||
for (var i in properties) {
|
||||
var p = properties[i];
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: '*',
|
||||
allowConnectionsOnly: true
|
||||
},
|
||||
plug: 'input/output',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + p,
|
||||
displayName: p
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: p + ' Changed',
|
||||
name: 'changed-' + p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'input/output'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: ModelNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.Model2', function (node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var NewDbModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'NewDbModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/create-new-record',
|
||||
displayName: 'Create New Record',
|
||||
usePortAsLabel: 'collectionName',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.storageInsert();
|
||||
}
|
||||
},
|
||||
sourceObjectId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Source Object Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
|
||||
this._internal.sourceObjectId = value; // Wait to fetch data
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
created: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
storageInsert: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (!this.checkWarningsBeforeCloudOp()) return;
|
||||
|
||||
this.scheduleOnce('StorageInsert', () => {
|
||||
const initValues = Object.assign(
|
||||
{},
|
||||
internal.sourceObjectId ? (this.nodeScope.modelScope || Model).get(internal.sourceObjectId).data : {},
|
||||
internal.inputValues
|
||||
);
|
||||
|
||||
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
|
||||
cloudstore.create({
|
||||
collection: internal.collectionId,
|
||||
data: initValues,
|
||||
acl: this._getACL(),
|
||||
success: (data) => {
|
||||
// Successfully created
|
||||
const m = cloudstore._fromJSON(data, internal.collectionId);
|
||||
this.setModel(m);
|
||||
this.sendSignalOnOutput('created');
|
||||
},
|
||||
error: (err) => {
|
||||
this.setError(err || 'Failed to insert.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(NewDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addModelId(NewDbModelPropertiedNodeDefinition, {
|
||||
includeOutputs: true
|
||||
});
|
||||
DbModelCRUDBase.addInputProperties(NewDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addAccessControl(NewDbModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = NewDbModelPropertiedNodeDefinition;
|
||||
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var ModelCRUDBase = require('./modelcrudbase');
|
||||
|
||||
var NewModelNodeDefinition = {
|
||||
node: {
|
||||
name: 'NewModel',
|
||||
docs: 'https://docs.noodl.net/nodes/data/object/create-new-object',
|
||||
displayNodeName: 'Create New Object',
|
||||
inputs: {
|
||||
new: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleNew();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
created: {
|
||||
type: 'signal',
|
||||
displayName: 'Done',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleNew: function () {
|
||||
if (this.hasScheduledNew) return;
|
||||
this.hasScheduledNew = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledNew = false;
|
||||
const newModel = (this.nodeScope.modelScope || Model).get();
|
||||
|
||||
this._pushInputValues(newModel);
|
||||
|
||||
this.setModel(newModel);
|
||||
|
||||
this.sendSignalOnOutput('created');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ModelCRUDBase.addBaseInfo(NewModelNodeDefinition);
|
||||
ModelCRUDBase.addModelId(NewModelNodeDefinition, { includeOutputs: true });
|
||||
ModelCRUDBase.addInputProperties(NewModelNodeDefinition);
|
||||
|
||||
module.exports = NewModelNodeDefinition;
|
||||
587
packages/noodl-runtime/src/nodes/std-library/data/restnode.js
Normal file
587
packages/noodl-runtime/src/nodes/std-library/data/restnode.js
Normal file
@@ -0,0 +1,587 @@
|
||||
var defaultRequestScript =
|
||||
'' +
|
||||
'//Add custom code to setup the request object before the request\n' +
|
||||
'//is made.\n' +
|
||||
'//\n' +
|
||||
'//*Request.resource contains the resource path of the request.\n' +
|
||||
'//*Request.method contains the method, GET, POST, PUT or DELETE.\n' +
|
||||
'//*Request.headers is a map where you can add additional headers.\n' +
|
||||
'//*Request.parameters is a map the parameters that will be appended\n' +
|
||||
'// to the url.\n' +
|
||||
'//*Request.content contains the content of the request as a javascript\n' +
|
||||
'// object.\n' +
|
||||
'//\n';
|
||||
('//*Inputs and *Outputs contain the inputs and outputs of the node.\n');
|
||||
|
||||
var defaultResponseScript =
|
||||
'' +
|
||||
'// Add custom code to convert the response content to outputs\n' +
|
||||
'//\n' +
|
||||
'//*Response.status The status code of the response\n' +
|
||||
'//*Response.content The content of the response as a javascript\n' +
|
||||
'// object.\n' +
|
||||
'//*Response.request The request object that resulted in the response.\n' +
|
||||
'//\n' +
|
||||
'//*Inputs and *Outputs contain the inputs and outputs of the node.\n';
|
||||
|
||||
var RestNode = {
|
||||
name: 'REST2',
|
||||
displayNodeName: 'REST',
|
||||
docs: 'https://docs.noodl.net/nodes/data/rest',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
searchTags: ['http', 'request', 'fetch'],
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
this._internal.outputValues = {};
|
||||
|
||||
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
|
||||
set: (obj, prop, value) => {
|
||||
//only send outputs when they change.
|
||||
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
|
||||
if (value !== this._internal.outputValues[prop]) {
|
||||
this.registerOutputIfNeeded('out-' + prop);
|
||||
|
||||
this._internal.outputValues[prop] = value;
|
||||
this.flagOutputDirty('out-' + prop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this._internal.self = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.inspectData
|
||||
? { type: 'value', value: this._internal.inspectData }
|
||||
: { type: 'text', value: '[Not executed yet]' };
|
||||
},
|
||||
inputs: {
|
||||
resource: {
|
||||
type: 'string',
|
||||
displayName: 'Resource',
|
||||
group: 'Request',
|
||||
default: '/',
|
||||
set: function (value) {
|
||||
this._internal.resource = value;
|
||||
}
|
||||
},
|
||||
method: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
{ label: 'DELETE', value: 'DELETE' }
|
||||
]
|
||||
},
|
||||
displayName: 'Method',
|
||||
group: 'Request',
|
||||
default: 'GET',
|
||||
set: function (value) {
|
||||
this._internal.method = value;
|
||||
}
|
||||
},
|
||||
/* scriptInputs: {
|
||||
type: { name: 'proplist', allowEditOnly: true },
|
||||
group: 'Inputs',
|
||||
set: function (value) {
|
||||
// this._internal.scriptInputs = value;
|
||||
}
|
||||
},
|
||||
scriptOutputs: {
|
||||
type: { name: 'proplist', allowEditOnly: true },
|
||||
group: 'Outputs',
|
||||
set: function (value) {
|
||||
// this._internal.scriptOutputs = value;
|
||||
}
|
||||
},*/
|
||||
requestScript: {
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
displayName: 'Request',
|
||||
default: defaultRequestScript,
|
||||
group: 'Scripts',
|
||||
set: function (script) {
|
||||
try {
|
||||
this._internal.requestFunc = new Function('Inputs', 'Outputs', 'Request', script);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
responseScript: {
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
displayName: 'Response',
|
||||
default: defaultResponseScript,
|
||||
group: 'Scripts',
|
||||
set: function (script) {
|
||||
try {
|
||||
this._internal.responseFunc = new Function('Inputs', 'Outputs', 'Response', script);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
fetch: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
type: 'signal',
|
||||
displayName: 'Cancel',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.cancelFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
canceled: {
|
||||
type: 'signal',
|
||||
displayName: 'Canceled',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
getScriptOutputValue: function (name) {
|
||||
return this._internal.outputValues[name];
|
||||
},
|
||||
setScriptInputValue: function (name, value) {
|
||||
return (this._internal.inputValues[name] = value);
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('out-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getScriptOutputValue.bind(this, name.substring('out-'.length))
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('in-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setScriptInputValue.bind(this, name.substring('in-'.length))
|
||||
});
|
||||
|
||||
/* if (name.startsWith('intype-')) return this.registerInput(name, {
|
||||
set: function() {} // Ignore input type
|
||||
})
|
||||
|
||||
if (name.startsWith('outtype-')) return this.registerInput(name, {
|
||||
set: function() {} // Ignore output type
|
||||
})*/
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledFetch) {
|
||||
internal.hasScheduledFetch = true;
|
||||
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
|
||||
}
|
||||
},
|
||||
doResponse: function (status, response, request) {
|
||||
// Process the response content with the response function
|
||||
if (this._internal.responseFunc) {
|
||||
this._internal.responseFunc.apply(this._internal.self, [
|
||||
this._internal.inputValues,
|
||||
this._internal.outputValuesProxy,
|
||||
{ status: status, content: response, request: request }
|
||||
]);
|
||||
}
|
||||
|
||||
this._internal.inspectData = { status: status, content: response };
|
||||
|
||||
// Flag status
|
||||
if (status >= 200 && status < 300) {
|
||||
this.sendSignalOnOutput('success');
|
||||
} else {
|
||||
this.sendSignalOnOutput('failure');
|
||||
}
|
||||
},
|
||||
doExternalFetch: function (request) {
|
||||
var url = request.resource;
|
||||
|
||||
// Append parameters from request as query
|
||||
if (Object.keys(request.parameters).length > 0) {
|
||||
var parameters = Object.keys(request.parameters).map(function (p) {
|
||||
return p + '=' + encodeURIComponent(request.parameters[p]);
|
||||
});
|
||||
url += '?' + parameters.join('&');
|
||||
}
|
||||
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// Running in browser
|
||||
var _this = this;
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
this._xhr = xhr;
|
||||
|
||||
xhr.open(request.method, url, true);
|
||||
for (var header in request.headers) {
|
||||
xhr.setRequestHeader(header, request.headers[header]);
|
||||
}
|
||||
xhr.onreadystatechange = function () {
|
||||
// XMLHttpRequest.DONE = 4, but torped runtime doesn't support enum
|
||||
|
||||
var sentResponse = false;
|
||||
|
||||
if (this.readyState === 4 || this.readyState === XMLHttpRequest.DONE) {
|
||||
var statusCode = this.status;
|
||||
var responseType = this.getResponseHeader('content-type');
|
||||
var rawResponse = this.response;
|
||||
delete this._xhr;
|
||||
|
||||
if (responseType) {
|
||||
responseType = responseType.toLowerCase();
|
||||
const responseData = responseType.indexOf('json') !== -1 ? JSON.parse(rawResponse) : rawResponse;
|
||||
|
||||
_this.doResponse(statusCode, responseData, request);
|
||||
sentResponse = true;
|
||||
}
|
||||
|
||||
if (sentResponse === false) {
|
||||
_this.doResponse(statusCode, rawResponse, request);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.onerror = function (e) {
|
||||
//console.log('REST: Failed to request', url);
|
||||
delete this._xhr;
|
||||
_this.sendSignalOnOutput('failure');
|
||||
};
|
||||
|
||||
xhr.onabort = function () {
|
||||
delete this._xhr;
|
||||
_this.sendSignalOnOutput('canceled');
|
||||
};
|
||||
|
||||
if (request.content) {
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify(request.content));
|
||||
} else {
|
||||
xhr.send();
|
||||
}
|
||||
} else {
|
||||
// Running in cloud runtime
|
||||
const headers = Object.assign(
|
||||
{},
|
||||
request.headers,
|
||||
request.content
|
||||
? {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
: {}
|
||||
);
|
||||
fetch(url, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: request.content ? JSON.stringify(request.content) : undefined
|
||||
})
|
||||
.then((response) => {
|
||||
const responseType = response.headers.get('content-type');
|
||||
if (responseType) {
|
||||
if (responseType.indexOf('/json') !== -1) {
|
||||
response.json().then((json) => {
|
||||
this.doResponse(response.status, json, request);
|
||||
});
|
||||
} else {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'rest-run-waring-',
|
||||
{
|
||||
message: 'REST only supports json content type in response.'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.text().then((raw) => {
|
||||
this.doResponse(response.status, raw, request);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('REST: Failed to request', url);
|
||||
console.log(e);
|
||||
this.sendSignalOnOutput('failure');
|
||||
});
|
||||
}
|
||||
},
|
||||
doFetch: function () {
|
||||
this._internal.hasScheduledFetch = false;
|
||||
|
||||
// Format resource path
|
||||
var resource = this._internal.resource;
|
||||
if (resource) {
|
||||
for (var key in this._internal.inputValues) {
|
||||
resource = resource.replace('{' + key + '}', this._internal.inputValues[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the request
|
||||
var request = {
|
||||
resource: resource,
|
||||
headers: {},
|
||||
method: this._internal.method !== undefined ? this._internal.method : 'GET',
|
||||
parameters: {}
|
||||
};
|
||||
|
||||
// Process the request content with the preprocess function
|
||||
if (this._internal.requestFunc) {
|
||||
this._internal.requestFunc.apply(this._internal.self, [
|
||||
this._internal.inputValues,
|
||||
this._internal.outputValuesProxy,
|
||||
request
|
||||
]);
|
||||
}
|
||||
|
||||
// Perform request
|
||||
this.doExternalFetch(request);
|
||||
},
|
||||
cancelFetch: function () {
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
this._xhr && this._xhr.abort();
|
||||
} else {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'rest-run-waring-', {
|
||||
message: "REST doesn't support cancel in cloud functions."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _parseScriptForErrors(script, args, name, node, context, ports) {
|
||||
// Clear run warnings if the script is edited
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-run-waring-' + name);
|
||||
|
||||
if (script === undefined) {
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-parse-waring-' + name);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new Function(...args, script);
|
||||
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-parse-waring-' + name);
|
||||
} catch (e) {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'rest-parse-waring-' + name, {
|
||||
message: '<strong>' + name + '</strong>: ' + e.message,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
|
||||
// Extract inputs and outputs
|
||||
function _exists(port) {
|
||||
return ports.find((p) => p.name === port) !== undefined;
|
||||
}
|
||||
|
||||
const scriptWithoutComments = script.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
|
||||
const inputs = scriptWithoutComments.match(/Inputs\.[A-Za-z0-9]+/g);
|
||||
if (inputs) {
|
||||
const unique = {};
|
||||
inputs.forEach((v) => {
|
||||
unique[v.substring('Inputs.'.length)] = true;
|
||||
});
|
||||
|
||||
Object.keys(unique).forEach((p) => {
|
||||
if (_exists('in-' + p)) return;
|
||||
|
||||
ports.push({
|
||||
name: 'in-' + p,
|
||||
displayName: p,
|
||||
plug: 'input',
|
||||
type: '*',
|
||||
group: 'Inputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const outputs = scriptWithoutComments.match(/Outputs\.[A-Za-z0-9]+/g);
|
||||
if (outputs) {
|
||||
const unique = {};
|
||||
outputs.forEach((v) => {
|
||||
unique[v.substring('Outputs.'.length)] = true;
|
||||
});
|
||||
|
||||
Object.keys(unique).forEach((p) => {
|
||||
if (_exists('out-' + p)) return;
|
||||
|
||||
ports.push({
|
||||
name: 'out-' + p,
|
||||
displayName: p,
|
||||
plug: 'output',
|
||||
type: '*',
|
||||
group: 'Outputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: RestNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
if (!node.parameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ports = [];
|
||||
function exists(name) {
|
||||
for (var i = 0; i < ports.length; i++) if (ports[i].name === name && ports[i].plug === 'input') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* const _typeEnums = [{value:'string',label:'String'},
|
||||
{value:'boolean',label:'Boolean'},
|
||||
{value:'number',label:'Number'},
|
||||
{value:'color',label:'Color'},
|
||||
{value:'object',label:'Object'},
|
||||
{value:'array',label:'Array'}]*/
|
||||
|
||||
// Inputs
|
||||
/* if (node.parameters['scriptInputs'] !== undefined && node.parameters['scriptInputs'].length > 0) {
|
||||
node.parameters['scriptInputs'].forEach((p) => {
|
||||
// Type for input
|
||||
ports.push({
|
||||
name: 'intype-' + p.label,
|
||||
displayName: 'Type',
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: _typeEnums, allowEditOnly: true },
|
||||
default: 'string',
|
||||
parent: 'scriptInputs',
|
||||
parentItemId: p.id
|
||||
})
|
||||
|
||||
// Default Value for input
|
||||
ports.push({
|
||||
name: 'in-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'input',
|
||||
type: node.parameters['intype-' + p.label] || 'string',
|
||||
group: 'Input Values'
|
||||
})
|
||||
})
|
||||
}*/
|
||||
|
||||
// Outputs
|
||||
/* if (node.parameters['scriptOutputs'] !== undefined && node.parameters['scriptOutputs'].length > 0) {
|
||||
node.parameters['scriptOutputs'].forEach((p) => {
|
||||
// Type for output
|
||||
ports.push({
|
||||
name: 'outtype-' + p.label,
|
||||
displayName: 'Type',
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: _typeEnums, allowEditOnly: true },
|
||||
default: 'string',
|
||||
parent: 'scriptOutputs',
|
||||
parentItemId: p.id
|
||||
})
|
||||
|
||||
// Value for output
|
||||
ports.push({
|
||||
name: 'out-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'output',
|
||||
type: node.parameters['outtype-' + p.label] || '*',
|
||||
group: 'Outputs',
|
||||
})
|
||||
})
|
||||
}*/
|
||||
|
||||
// Parse resource path inputs
|
||||
if (node.parameters.resource) {
|
||||
var inputs = node.parameters.resource.match(/\{[A-Za-z0-9_]*\}/g);
|
||||
for (var i in inputs) {
|
||||
var def = inputs[i];
|
||||
var name = def.replace('{', '').replace('}', '');
|
||||
if (exists('in-' + name)) continue;
|
||||
|
||||
ports.push({
|
||||
name: 'in-' + name,
|
||||
displayName: name,
|
||||
type: 'string',
|
||||
plug: 'input',
|
||||
group: 'Inputs'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (node.parameters['requestScript']) {
|
||||
_parseScriptForErrors(
|
||||
node.parameters['requestScript'],
|
||||
['Inputs', 'Outputs', 'Request'],
|
||||
'Request script',
|
||||
node,
|
||||
context,
|
||||
ports
|
||||
);
|
||||
}
|
||||
|
||||
if (node.parameters['responseScript']) {
|
||||
_parseScriptForErrors(
|
||||
node.parameters['responseScript'],
|
||||
['Inputs', 'Outputs', 'Response'],
|
||||
'Response script',
|
||||
node,
|
||||
context,
|
||||
ports
|
||||
);
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function () {
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.REST2', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('REST2')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var SetDbModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'SetDbModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/set-record-properties',
|
||||
displayNodeName: 'Set Record Properties',
|
||||
usePortAsLabel: 'collectionName',
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'storeType = cloud OR storeType NOT SET',
|
||||
inputs: ['storeProperties']
|
||||
}
|
||||
],
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.storeType === undefined || this._internal.storeType === 'cloud') this.scheduleSave();
|
||||
else this.scheduleStore();
|
||||
}
|
||||
},
|
||||
storeProperties: {
|
||||
displayName: 'Properties to store',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Only specified', value: 'specified' },
|
||||
{ label: 'All', value: 'all' }
|
||||
]
|
||||
},
|
||||
default: 'specified',
|
||||
set: function (value) {
|
||||
this._internal.storeProperties = value;
|
||||
}
|
||||
},
|
||||
storeType: {
|
||||
displayName: 'Store to',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Cloud and local', value: 'cloud' },
|
||||
{ label: 'Local only', value: 'local' }
|
||||
]
|
||||
},
|
||||
default: 'cloud',
|
||||
set: function (value) {
|
||||
this._internal.storeType = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
stored: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleSave: function () {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
if (!this.checkWarningsBeforeCloudOp()) return;
|
||||
|
||||
this.scheduleOnce('StorageSave', function () {
|
||||
if (!internal.model) {
|
||||
_this.setError('Missing Record Id');
|
||||
return;
|
||||
}
|
||||
var model = internal.model;
|
||||
|
||||
for (var i in internal.inputValues) {
|
||||
model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.modelScope).save({
|
||||
collection: internal.collectionId,
|
||||
objectId: model.getId(), // Get the objectId part of the model id
|
||||
data: internal.storeProperties === 'all' ? model.data : internal.inputValues, // Only store input values by default, if not explicitly specified
|
||||
acl: _this._getACL(),
|
||||
success: function (response) {
|
||||
for (var key in response) {
|
||||
model.set(key, response[key]);
|
||||
}
|
||||
|
||||
_this.sendSignalOnOutput('stored');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to save.');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
scheduleStore: function () {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledStore = false;
|
||||
if (!internal.model) return;
|
||||
|
||||
for (var i in internal.inputValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
this.sendSignalOnOutput('stored');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(SetDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addModelId(SetDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addInputProperties(SetDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addAccessControl(SetDbModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = SetDbModelPropertiedNodeDefinition;
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
var ModelCRUDBase = require('./modelcrudbase');
|
||||
|
||||
var SetModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'SetModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/object/set-object-properties',
|
||||
displayNodeName: 'Set Object Properties',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleStore();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
stored: {
|
||||
type: 'signal',
|
||||
displayName: 'Done',
|
||||
group: 'Events'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ModelCRUDBase.addBaseInfo(SetModelPropertiedNodeDefinition);
|
||||
ModelCRUDBase.addModelId(SetModelPropertiedNodeDefinition);
|
||||
ModelCRUDBase.addInputProperties(SetModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = SetModelPropertiedNodeDefinition;
|
||||
80
packages/noodl-runtime/src/nodes/std-library/datetostring.js
Normal file
80
packages/noodl-runtime/src/nodes/std-library/datetostring.js
Normal file
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const DateToStringNode = {
|
||||
name: 'Date To String',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/date-to-string',
|
||||
category: 'Utilities',
|
||||
initialize: function () {
|
||||
this._internal.formatString = '{year}-{month}-{date}';
|
||||
},
|
||||
inputs: {
|
||||
formatString: {
|
||||
displayName: 'Format',
|
||||
type: 'string',
|
||||
default: '{year}-{month}-{date}',
|
||||
set: function (value) {
|
||||
if (this._internal.formatString === value) return;
|
||||
this._internal.formatString = value;
|
||||
|
||||
if (this._internal.currentInput !== undefined) {
|
||||
this._format();
|
||||
this.flagOutputDirty('currentValue');
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
type: { name: 'date' },
|
||||
displayName: 'Date',
|
||||
set: function (value) {
|
||||
const _value = typeof value === 'string' ? new Date(value) : value;
|
||||
if (this._internal.currentInput === _value) return;
|
||||
|
||||
this._internal.currentInput = _value;
|
||||
this._format();
|
||||
this.flagOutputDirty('currentValue');
|
||||
this.sendSignalOnOutput('inputChanged');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentValue: {
|
||||
type: 'string',
|
||||
displayName: 'Date String',
|
||||
group: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.dateString;
|
||||
}
|
||||
},
|
||||
inputChanged: {
|
||||
type: 'signal',
|
||||
displayName: 'Date Changed',
|
||||
group: 'Signals'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_format() {
|
||||
const t = this._internal.currentInput;
|
||||
const format = this._internal.formatString;
|
||||
const date = ('0' + t.getDate()).slice(-2);
|
||||
const month = ('0' + (t.getMonth() + 1)).slice(-2);
|
||||
const monthShort = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(t);
|
||||
const year = t.getFullYear();
|
||||
const hours = ('0' + t.getHours()).slice(-2);
|
||||
const minutes = ('0' + t.getMinutes()).slice(-2);
|
||||
const seconds = ('0' + t.getSeconds()).slice(-2);
|
||||
|
||||
this._internal.dateString = format
|
||||
.replace(/\{date\}/g, date)
|
||||
.replace(/\{month\}/g, month)
|
||||
.replace(/\{monthShort\}/g, monthShort)
|
||||
.replace(/\{year\}/g, year)
|
||||
.replace(/\{hours\}/g, hours)
|
||||
.replace(/\{minutes\}/g, minutes)
|
||||
.replace(/\{seconds\}/g, seconds);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: DateToStringNode
|
||||
};
|
||||
359
packages/noodl-runtime/src/nodes/std-library/expression.js
Normal file
359
packages/noodl-runtime/src/nodes/std-library/expression.js
Normal file
@@ -0,0 +1,359 @@
|
||||
'use strict';
|
||||
|
||||
const difference = require('lodash.difference');
|
||||
|
||||
//const Model = require('./data/model');
|
||||
|
||||
const ExpressionNode = {
|
||||
name: 'Expression',
|
||||
docs: 'https://docs.noodl.net/nodes/math/expression',
|
||||
usePortAsLabel: 'expression',
|
||||
category: 'CustomCode',
|
||||
color: 'javascript',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'Expression'
|
||||
},
|
||||
searchTags: ['javascript'],
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.scope = {};
|
||||
internal.hasScheduledEvaluation = false;
|
||||
|
||||
internal.code = undefined;
|
||||
internal.cachedValue = 0;
|
||||
internal.currentExpression = '';
|
||||
internal.compiledFunction = undefined;
|
||||
internal.inputNames = [];
|
||||
internal.inputValues = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.cachedValue;
|
||||
},
|
||||
inputs: {
|
||||
expression: {
|
||||
group: 'General',
|
||||
inputPriority: 1,
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
displayName: 'Expression',
|
||||
set: function (value) {
|
||||
var internal = this._internal;
|
||||
internal.currentExpression = functionPreamble + 'return (' + value + ');';
|
||||
internal.compiledFunction = undefined;
|
||||
|
||||
var newInputs = parsePorts(value);
|
||||
|
||||
var inputsToAdd = difference(newInputs, internal.inputNames);
|
||||
var inputsToRemove = difference(internal.inputNames, newInputs);
|
||||
|
||||
var self = this;
|
||||
inputsToRemove.forEach(function (name) {
|
||||
self.deregisterInput(name);
|
||||
delete internal.scope[name];
|
||||
});
|
||||
|
||||
inputsToAdd.forEach(function (name) {
|
||||
if (self.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.registerInput(name, {
|
||||
set: function (value) {
|
||||
internal.scope[name] = value;
|
||||
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
|
||||
}
|
||||
});
|
||||
|
||||
internal.scope[name] = 0;
|
||||
self._inputValues[name] = 0;
|
||||
});
|
||||
|
||||
/* if(value.indexOf('Vars') !== -1 || value.indexOf('Variables') !== -1) {
|
||||
// This expression is using variables, it should listen for changes
|
||||
this._internal.onVariablesChangedCallback = (args) => {
|
||||
this._scheduleEvaluateExpression()
|
||||
}
|
||||
|
||||
Model.get('--ndl--global-variables').off('change',this._internal.onVariablesChangedCallback)
|
||||
Model.get('--ndl--global-variables').on('change',this._internal.onVariablesChangedCallback)
|
||||
}*/
|
||||
|
||||
internal.inputNames = Object.keys(internal.scope);
|
||||
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
|
||||
}
|
||||
},
|
||||
run: {
|
||||
group: 'Actions',
|
||||
displayName: 'Run',
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () {
|
||||
this._scheduleEvaluateExpression();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
group: 'Result',
|
||||
type: '*',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
if (!this._internal.currentExpression) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this._internal.cachedValue;
|
||||
}
|
||||
},
|
||||
isTrue: {
|
||||
group: 'Result',
|
||||
type: 'boolean',
|
||||
displayName: 'Is True',
|
||||
getter: function () {
|
||||
if (!this._internal.currentExpression) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!this._internal.cachedValue;
|
||||
}
|
||||
},
|
||||
isFalse: {
|
||||
group: 'Result',
|
||||
type: 'boolean',
|
||||
displayName: 'Is False',
|
||||
getter: function () {
|
||||
if (!this._internal.currentExpression) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this._internal.cachedValue;
|
||||
}
|
||||
},
|
||||
isTrueEv: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'On True'
|
||||
},
|
||||
isFalseEv: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'On False'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
registerInputIfNeeded: {
|
||||
value: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.scope[name] = 0;
|
||||
this._inputValues[name] = 0;
|
||||
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
this._internal.scope[name] = value;
|
||||
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
_scheduleEvaluateExpression: {
|
||||
value: function () {
|
||||
var internal = this._internal;
|
||||
if (internal.hasScheduledEvaluation === false) {
|
||||
internal.hasScheduledEvaluation = true;
|
||||
this.flagDirty();
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
var lastValue = internal.cachedValue;
|
||||
internal.cachedValue = this._calculateExpression();
|
||||
if (lastValue !== internal.cachedValue) {
|
||||
this.flagOutputDirty('result');
|
||||
this.flagOutputDirty('isTrue');
|
||||
this.flagOutputDirty('isFalse');
|
||||
}
|
||||
if (internal.cachedValue) this.sendSignalOnOutput('isTrueEv');
|
||||
else this.sendSignalOnOutput('isFalseEv');
|
||||
internal.hasScheduledEvaluation = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
_calculateExpression: {
|
||||
value: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
if (!internal.compiledFunction) {
|
||||
internal.compiledFunction = this._compileFunction();
|
||||
}
|
||||
for (var i = 0; i < internal.inputNames.length; ++i) {
|
||||
var inputValue = internal.scope[internal.inputNames[i]];
|
||||
internal.inputValues[i] = inputValue;
|
||||
}
|
||||
try {
|
||||
return internal.compiledFunction.apply(null, internal.inputValues);
|
||||
} catch (e) {
|
||||
console.error('Error in expression:', e.message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
_compileFunction: {
|
||||
value: function () {
|
||||
var expression = this._internal.currentExpression;
|
||||
var args = Object.keys(this._internal.scope);
|
||||
|
||||
var key = expression + args.join(' ');
|
||||
|
||||
if (compiledFunctionsCache.hasOwnProperty(key) === false) {
|
||||
args.push(expression);
|
||||
|
||||
try {
|
||||
compiledFunctionsCache[key] = construct(Function, args);
|
||||
} catch (e) {
|
||||
console.error('Failed to compile JS function', e.message);
|
||||
}
|
||||
}
|
||||
return compiledFunctionsCache[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var functionPreamble = [
|
||||
'var min = Math.min,' +
|
||||
' max = Math.max,' +
|
||||
' cos = Math.cos,' +
|
||||
' sin = Math.sin,' +
|
||||
' tan = Math.tan,' +
|
||||
' sqrt = Math.sqrt,' +
|
||||
' pi = Math.PI,' +
|
||||
' round = Math.round,' +
|
||||
' floor = Math.floor,' +
|
||||
' ceil = Math.ceil,' +
|
||||
' abs = Math.abs,' +
|
||||
' random = Math.random;'
|
||||
/* ' Vars = Variables = Noodl.Object.get("--ndl--global-variables");' */
|
||||
].join('');
|
||||
|
||||
//Since apply cannot be used on constructors (i.e. new Something) we need this hax
|
||||
//see http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
|
||||
function construct(constructor, args) {
|
||||
function F() {
|
||||
return constructor.apply(this, args);
|
||||
}
|
||||
F.prototype = constructor.prototype;
|
||||
return new F();
|
||||
}
|
||||
|
||||
var compiledFunctionsCache = {};
|
||||
|
||||
var portsToIgnore = [
|
||||
'min',
|
||||
'max',
|
||||
'cos',
|
||||
'sin',
|
||||
'tan',
|
||||
'sqrt',
|
||||
'pi',
|
||||
'round',
|
||||
'floor',
|
||||
'ceil',
|
||||
'abs',
|
||||
'random',
|
||||
'Math',
|
||||
'window',
|
||||
'document',
|
||||
'undefined',
|
||||
'Vars',
|
||||
'true',
|
||||
'false',
|
||||
'null',
|
||||
'Boolean'
|
||||
];
|
||||
|
||||
function parsePorts(expression) {
|
||||
var ports = [];
|
||||
|
||||
function addPort(name) {
|
||||
if (portsToIgnore.indexOf(name) !== -1) return;
|
||||
if (
|
||||
ports.some(function (p) {
|
||||
return p === name;
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
ports.push(name);
|
||||
}
|
||||
|
||||
// First remove all strings
|
||||
expression = expression.replace(/\"([^\"]*)\"/g, '').replace(/\'([^\']*)\'/g, '');
|
||||
|
||||
// Extract identifiers
|
||||
var identifiers = expression.matchAll(/[a-zA-Z\_\$][a-zA-Z0-9\.\_\$]*/g);
|
||||
for (const _id of identifiers) {
|
||||
var name = _id[0];
|
||||
if (name.indexOf('.') !== -1) {
|
||||
name = name.split('.')[0]; // Take first symbol on "." sequence
|
||||
}
|
||||
|
||||
addPort(name);
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, expression, editorConnection) {
|
||||
var portNames = parsePorts(expression);
|
||||
|
||||
var ports = portNames.map(function (name) {
|
||||
return {
|
||||
group: 'Parameters',
|
||||
name: name,
|
||||
type: {
|
||||
name: '*',
|
||||
editAsType: 'string'
|
||||
},
|
||||
plug: 'input'
|
||||
};
|
||||
});
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
function evalCompileWarnings(editorConnection, node) {
|
||||
try {
|
||||
new Function(node.parameters.expression);
|
||||
editorConnection.clearWarning(node.component.name, node.id, 'expression-compile-error');
|
||||
} catch (e) {
|
||||
editorConnection.sendWarning(node.component.name, node.id, 'expression-compile-error', {
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: ExpressionNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.Expression', function (node) {
|
||||
if (node.parameters.expression) {
|
||||
updatePorts(node.id, node.parameters.expression, context.editorConnection);
|
||||
evalCompileWarnings(context.editorConnection, node);
|
||||
}
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'expression') {
|
||||
updatePorts(node.id, node.parameters.expression, context.editorConnection);
|
||||
evalCompileWarnings(context.editorConnection, node);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
45
packages/noodl-runtime/src/nodes/std-library/inverter.js
Normal file
45
packages/noodl-runtime/src/nodes/std-library/inverter.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
function invert(value) {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return !value;
|
||||
}
|
||||
|
||||
const InverterNode = {
|
||||
name: 'Inverter',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/inverter',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.currentValue = undefined;
|
||||
},
|
||||
getInspectInfo() {
|
||||
return String(invert(this._internal.currentValue));
|
||||
},
|
||||
inputs: {
|
||||
value: {
|
||||
type: {
|
||||
name: 'boolean'
|
||||
},
|
||||
displayName: 'Value',
|
||||
set: function (value) {
|
||||
this._internal.currentValue = value;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
return invert(this._internal.currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: InverterNode
|
||||
};
|
||||
46
packages/noodl-runtime/src/nodes/std-library/or.js
Normal file
46
packages/noodl-runtime/src/nodes/std-library/or.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const OrNode = {
|
||||
name: 'Or',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/or',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.inputs.some(isTrue);
|
||||
},
|
||||
numberedInputs: {
|
||||
input: {
|
||||
type: 'boolean',
|
||||
displayPrefix: 'Input',
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
if (this._internal.inputs[index] === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.inputs[index] = value;
|
||||
this.flagOutputDirty('result');
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
return this._internal.inputs.some(isTrue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: OrNode
|
||||
};
|
||||
|
||||
function isTrue(value) {
|
||||
return value ? true : false;
|
||||
}
|
||||
325
packages/noodl-runtime/src/nodes/std-library/runtasks.js
Normal file
325
packages/noodl-runtime/src/nodes/std-library/runtasks.js
Normal file
@@ -0,0 +1,325 @@
|
||||
const { Node } = require('../../../noodl-runtime');
|
||||
const guid = require('../../guid');
|
||||
const Model = require('../../model');
|
||||
|
||||
function sendSignalOnInput(itemNode, name) {
|
||||
itemNode.queueInput(name, true); // send signal
|
||||
itemNode.queueInput(name, false);
|
||||
}
|
||||
|
||||
const RunTasksDefinition = {
|
||||
name: 'RunTasks',
|
||||
displayNodeName: 'Run Tasks',
|
||||
docs: 'https://docs.noodl.net/nodes/data/run-tasks',
|
||||
color: 'data',
|
||||
category: 'Data',
|
||||
initialize() {
|
||||
this._internal.queuedOperations = [];
|
||||
this._internal.state = 'idle';
|
||||
this._internal.maxRunningTasks = 10;
|
||||
this._internal.activeTasks = new Map(); //id => ComponentInstanceNode
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
group: 'Data',
|
||||
displayName: 'Items',
|
||||
type: 'array',
|
||||
set: function (value) {
|
||||
if (!value) return;
|
||||
if (value === this._internal.items) return;
|
||||
|
||||
this._internal.items = value;
|
||||
}
|
||||
},
|
||||
stopOnFailure: {
|
||||
group: 'General',
|
||||
displayName: 'Stop On Failure',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
set: function (value) {
|
||||
this._internal.stopOnFailure = value;
|
||||
}
|
||||
},
|
||||
maxRunningTasks: {
|
||||
group: 'General',
|
||||
displayName: 'Max Running Tasks',
|
||||
type: 'number',
|
||||
default: 10,
|
||||
set: function (value) {
|
||||
this._internal.maxRunningTasks = value;
|
||||
}
|
||||
},
|
||||
taskTemplate: {
|
||||
type: 'component',
|
||||
displayName: 'Template',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.template = value;
|
||||
}
|
||||
},
|
||||
run: {
|
||||
group: 'General',
|
||||
displayName: 'Do',
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRun();
|
||||
}
|
||||
},
|
||||
abort: {
|
||||
group: 'General',
|
||||
displayName: 'Abort',
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleAbort();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Success'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Failure'
|
||||
},
|
||||
done: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Done'
|
||||
},
|
||||
aborted: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Aborted'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleRun() {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledRun) {
|
||||
internal.hasScheduledRun = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this._queueOperation(() => {
|
||||
internal.hasScheduledRun = false;
|
||||
this.run();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
scheduleAbort() {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledAbort) {
|
||||
internal.hasScheduledAbort = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this._queueOperation(() => {
|
||||
internal.hasScheduledAbort = false;
|
||||
this.abort();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
async createTaskComponent(item) {
|
||||
const internal = this._internal;
|
||||
|
||||
const modelScope = this.nodeScope.modelScope || Model;
|
||||
const model = modelScope.create(item);
|
||||
|
||||
var itemNode = await this.nodeScope.createNode(internal.template, guid(), {
|
||||
_forEachModel: model,
|
||||
_forEachNode: this
|
||||
});
|
||||
|
||||
// This is needed to make sure any action connected to "Do"
|
||||
// is not run directly
|
||||
const _isInputConnected = itemNode.isInputConnected.bind(itemNode);
|
||||
itemNode.isInputConnected = (name) => {
|
||||
if (name === 'Do') return true;
|
||||
return _isInputConnected(name);
|
||||
};
|
||||
|
||||
// Set the Id as an input
|
||||
if (itemNode.hasInput('Id')) {
|
||||
itemNode.setInputValue('Id', model.getId());
|
||||
}
|
||||
if (itemNode.hasInput('id')) {
|
||||
itemNode.setInputValue('id', model.getId());
|
||||
}
|
||||
|
||||
// Push all other values also as inputs
|
||||
// if they exist as component inputs
|
||||
for (var inputKey in itemNode._inputs) {
|
||||
if (model.data[inputKey] !== undefined) itemNode.setInputValue(inputKey, model.data[inputKey]);
|
||||
}
|
||||
|
||||
// capture signals
|
||||
itemNode._internal.creatorCallbacks = {
|
||||
onOutputChanged: (name, value, oldValue) => {
|
||||
if ((oldValue === false || oldValue === undefined) && value === true) {
|
||||
this.itemOutputSignalTriggered(name, model, itemNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return itemNode;
|
||||
},
|
||||
async startTask(task) {
|
||||
const internal = this._internal;
|
||||
|
||||
try {
|
||||
const taskComponent = await this.createTaskComponent(task);
|
||||
internal.runningTasks++;
|
||||
sendSignalOnInput(taskComponent, 'Do');
|
||||
internal.activeTasks.set(taskComponent.id, taskComponent);
|
||||
} catch (e) {
|
||||
// Something went wrong starting the task
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
async run() {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
if (internal.state !== 'idle') {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks', {
|
||||
message: 'Cannot start when not in idle mode'
|
||||
});
|
||||
} else if (!internal.template) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks', {
|
||||
message: 'No task template specified.'
|
||||
});
|
||||
} else if (!internal.items) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks', {
|
||||
message: 'No items array provided.'
|
||||
});
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks');
|
||||
}
|
||||
}
|
||||
|
||||
if (internal.state !== 'idle') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!internal.template) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!internal.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
internal.state = 'running';
|
||||
internal.numTasks = internal.items.length;
|
||||
internal.failedTasks = 0;
|
||||
internal.completedTasks = 0;
|
||||
internal.queuedTasks = [].concat(internal.items);
|
||||
internal.runningTasks = 0;
|
||||
|
||||
// No tasks
|
||||
if (internal.items.length === 0) {
|
||||
this.sendSignalOnOutput('success');
|
||||
internal.state = 'idle';
|
||||
}
|
||||
|
||||
// Start tasks
|
||||
for (let i = 0; i < Math.min(internal.maxRunningTasks, internal.queuedTasks.length); i++) {
|
||||
const task = internal.queuedTasks.shift();
|
||||
if (!task) break;
|
||||
|
||||
this.startTask(task);
|
||||
}
|
||||
},
|
||||
abort: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
internal.state = 'aborted';
|
||||
},
|
||||
itemOutputSignalTriggered: function (name, model, itemNode) {
|
||||
const internal = this._internal;
|
||||
|
||||
if (internal.state === 'idle') {
|
||||
// Signal while we are not running is ignored
|
||||
return;
|
||||
}
|
||||
|
||||
const checkDone = () => {
|
||||
if (internal.state === 'aborted') {
|
||||
this.sendSignalOnOutput('aborted');
|
||||
internal.state = 'idle';
|
||||
return;
|
||||
}
|
||||
|
||||
if (internal.completedTasks === internal.numTasks) {
|
||||
if (internal.failedTasks === 0) this.sendSignalOnOutput('success');
|
||||
else this.sendSignalOnOutput('failure');
|
||||
this.sendSignalOnOutput('done');
|
||||
internal.state = 'idle';
|
||||
} else {
|
||||
if (internal.stopOnFailure) {
|
||||
// Only continue if there are no failed tasks, otherwise aborted
|
||||
if (internal.failedTasks === 0) {
|
||||
internal.runningTasks++;
|
||||
const task = internal.queuedTasks.shift();
|
||||
if (task) this.startTask(task);
|
||||
} else {
|
||||
this.sendSignalOnOutput('failure');
|
||||
this.sendSignalOnOutput('aborted');
|
||||
}
|
||||
} else {
|
||||
internal.runningTasks++;
|
||||
const task = internal.queuedTasks.shift();
|
||||
if (task) this.startTask(task);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (name === 'Success') {
|
||||
internal.completedTasks++;
|
||||
internal.runningTasks--;
|
||||
checkDone();
|
||||
} else if (name === 'Failure') {
|
||||
internal.completedTasks++;
|
||||
internal.failedTasks++;
|
||||
internal.runningTasks--;
|
||||
checkDone();
|
||||
}
|
||||
|
||||
internal.activeTasks.delete(itemNode.id);
|
||||
this.nodeScope.deleteNode(itemNode);
|
||||
},
|
||||
_queueOperation(op) {
|
||||
this._internal.queuedOperations.push(op);
|
||||
this._runQueueOperations();
|
||||
},
|
||||
async _runQueueOperations() {
|
||||
if (this.runningOperations) {
|
||||
return;
|
||||
}
|
||||
this.runningOperations = true;
|
||||
|
||||
while (this._internal.queuedOperations.length) {
|
||||
const op = this._internal.queuedOperations.shift();
|
||||
await op();
|
||||
}
|
||||
|
||||
this.runningOperations = false;
|
||||
}
|
||||
},
|
||||
_deleteAllTasks() {
|
||||
for (const taskComponent of this._internal.activeTasks) {
|
||||
this.nodeScope.deleteNode(taskComponent);
|
||||
}
|
||||
this._internal.activeTasks.clear();
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this._deleteAllTasks();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: RunTasksDefinition
|
||||
};
|
||||
416
packages/noodl-runtime/src/nodes/std-library/simplejavascript.js
Normal file
416
packages/noodl-runtime/src/nodes/std-library/simplejavascript.js
Normal file
@@ -0,0 +1,416 @@
|
||||
const JavascriptNodeParser = require('../../javascriptnodeparser');
|
||||
|
||||
const SimpleJavascriptNode = {
|
||||
name: 'JavaScriptFunction',
|
||||
displayNodeName: 'Function',
|
||||
docs: 'https://docs.noodl.net/nodes/javascript/function',
|
||||
category: 'CustomCode',
|
||||
color: 'javascript',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'Script'
|
||||
},
|
||||
searchTags: ['javascript'],
|
||||
exportDynamicPorts: true,
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
this._internal.outputValues = {};
|
||||
|
||||
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
|
||||
set: (obj, prop, value) => {
|
||||
//a function node can continue running after it has been deleted. E.g. with timeouts or event listeners that hasn't been removed.
|
||||
//if the node is deleted, just do nothing
|
||||
if (this._deleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
//only send outputs when they change.
|
||||
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
|
||||
if (value !== this._internal.outputValues[prop]) {
|
||||
this.registerOutputIfNeeded('out-' + prop);
|
||||
|
||||
this._internal.outputValues[prop] = value;
|
||||
this.flagOutputDirty('out-' + prop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this._internal._this = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value: {
|
||||
inputs: this._internal.inputValues,
|
||||
outputs: this._internal.outputValues
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
scriptInputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Inputs',
|
||||
set(value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
scriptOutputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Outputs',
|
||||
set(value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
functionScript: {
|
||||
displayName: 'Script',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
group: 'General',
|
||||
set(script) {
|
||||
if (script === undefined) {
|
||||
this._internal.func = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.func = this.parseScript(script);
|
||||
|
||||
if (!this.isInputConnected('run')) this.scheduleRun();
|
||||
}
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRun();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
methods: {
|
||||
scheduleRun: function () {
|
||||
if (this.runScheduled) return;
|
||||
this.runScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.runScheduled = false;
|
||||
|
||||
if (!this._deleted) {
|
||||
this.runScript();
|
||||
}
|
||||
});
|
||||
},
|
||||
runScript: async function () {
|
||||
const func = this._internal.func;
|
||||
|
||||
if (func === undefined) return;
|
||||
|
||||
const inputs = this._internal.inputValues;
|
||||
const outputs = this._internal.outputValuesProxy;
|
||||
|
||||
// Prepare send signal functions
|
||||
for (const key in this.model.outputPorts) {
|
||||
if (this._isSignalType(key)) {
|
||||
const _sendSignal = () => {
|
||||
if (this.hasOutput(key)) this.sendSignalOnOutput(key);
|
||||
};
|
||||
this._internal.outputValues[key.substring('out-'.length)] = _sendSignal;
|
||||
this._internal.outputValues[key.substring('out-'.length)].send = _sendSignal;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await func.apply(this._internal._this, [
|
||||
inputs,
|
||||
outputs,
|
||||
JavascriptNodeParser.createNoodlAPI(this.nodeScope.modelScope),
|
||||
JavascriptNodeParser.getComponentScopeForNode(this)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'Error in JS node run code.',
|
||||
Object.getPrototypeOf(e).constructor.name + ': ' + e.message,
|
||||
e.stack
|
||||
);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('javascriptExecution')) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'js-function-run-waring',
|
||||
{
|
||||
showGlobally: true,
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
setScriptInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
|
||||
if (!this.isInputConnected('run')) this.scheduleRun();
|
||||
},
|
||||
getScriptOutputValue: function (name) {
|
||||
if (this._isSignalType(name)) {
|
||||
return undefined;
|
||||
}
|
||||
return this._internal.outputValues[name];
|
||||
},
|
||||
setScriptInputType: function (name, type) {
|
||||
this._internal.inputTypes[name] = type;
|
||||
},
|
||||
setScriptOutputType: function (name, type) {
|
||||
this._internal.outputTypes[name] = type;
|
||||
},
|
||||
parseScript: function (script) {
|
||||
var func;
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
func = new AsyncFunction(
|
||||
'Inputs',
|
||||
'Outputs',
|
||||
'Noodl',
|
||||
'Component',
|
||||
JavascriptNodeParser.getCodePrefix() + script
|
||||
);
|
||||
} catch (e) {
|
||||
console.log('Error while parsing action script: ' + e);
|
||||
}
|
||||
|
||||
return func;
|
||||
},
|
||||
_isSignalType: function (name) {
|
||||
// This will catch signals in script that may not have been delivered by the editor yet
|
||||
return this.model.outputPorts[name] && this.model.outputPorts[name].type === 'signal';
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('in-')) {
|
||||
const n = name.substring('in-'.length);
|
||||
|
||||
const input = {
|
||||
set: this.setScriptInputValue.bind(this, n)
|
||||
};
|
||||
|
||||
//make sure we register the type as well, so Noodl resolves types like color styles to an actual color
|
||||
if (this.model && this.model.parameters['intype-' + n]) {
|
||||
input.type = this.model.parameters['intype-' + n];
|
||||
}
|
||||
|
||||
this.registerInput(name, input);
|
||||
}
|
||||
|
||||
if (name.startsWith('intype-')) {
|
||||
const n = name.substring('intype-'.length);
|
||||
|
||||
this.registerInput(name, {
|
||||
set(value) {
|
||||
//make sure we register the type as well, so Noodl resolves types like color styles to an actual color
|
||||
if (this.hasInput('in' + n)) {
|
||||
this.getInput('in' + n).type = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (name.startsWith('outtype-')) {
|
||||
this.registerInput(name, {
|
||||
set() {} // Ignore
|
||||
});
|
||||
}
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('out-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getScriptOutputValue.bind(this, name.substring('out-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _parseScriptForErrorsAndPorts(script, name, node, context, ports) {
|
||||
// Clear run warnings if the script is edited
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-run-waring');
|
||||
|
||||
if (script === undefined) {
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-parse-waring');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
new AsyncFunction('Inputs', 'Outputs', 'Noodl', 'Component', script);
|
||||
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-parse-waring');
|
||||
} catch (e) {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'js-function-parse-waring', {
|
||||
showGlobally: true,
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
|
||||
JavascriptNodeParser.parseAndAddPortsFromScript(script, ports, {
|
||||
inputPrefix: 'in-',
|
||||
outputPrefix: 'out-'
|
||||
});
|
||||
}
|
||||
|
||||
const inputTypeEnums = [
|
||||
{
|
||||
value: 'string',
|
||||
label: 'String'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
label: 'Boolean'
|
||||
},
|
||||
{
|
||||
value: 'number',
|
||||
label: 'Number'
|
||||
},
|
||||
{
|
||||
value: 'object',
|
||||
label: 'Object'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: 'Date'
|
||||
},
|
||||
{
|
||||
value: 'array',
|
||||
label: 'Array'
|
||||
},
|
||||
{
|
||||
value: 'color',
|
||||
label: 'Color'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
node: SimpleJavascriptNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const _outputTypeEnums = inputTypeEnums.concat([
|
||||
{
|
||||
value: 'signal',
|
||||
label: 'Signal'
|
||||
}
|
||||
]);
|
||||
|
||||
// Outputs
|
||||
if (node.parameters['scriptOutputs'] !== undefined && node.parameters['scriptOutputs'].length > 0) {
|
||||
node.parameters['scriptOutputs'].forEach((p) => {
|
||||
// Type for output
|
||||
ports.push({
|
||||
name: 'outtype-' + p.label,
|
||||
displayName: 'Type',
|
||||
editorName: p.label + ' | Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _outputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptOutputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Value for output
|
||||
ports.push({
|
||||
name: 'out-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'output',
|
||||
type: node.parameters['outtype-' + p.label] || '*',
|
||||
group: 'Outputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inputs
|
||||
if (node.parameters['scriptInputs'] !== undefined && node.parameters['scriptInputs'].length > 0) {
|
||||
node.parameters['scriptInputs'].forEach((p) => {
|
||||
// Type for input
|
||||
ports.push({
|
||||
name: 'intype-' + p.label,
|
||||
displayName: 'Type',
|
||||
editorName: p.label + ' | Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: inputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptInputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Default Value for input
|
||||
ports.push({
|
||||
name: 'in-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'input',
|
||||
type: node.parameters['intype-' + p.label] || 'string',
|
||||
group: 'Inputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_parseScriptForErrorsAndPorts(node.parameters['functionScript'], 'Script ', node, context, ports);
|
||||
|
||||
// Push output ports that are signals directly to the model, it's needed by the initial run of
|
||||
// the script function
|
||||
ports.forEach((p) => {
|
||||
if (p.type === 'signal' && p.plug === 'output') {
|
||||
node.outputPorts[p.name] = p;
|
||||
}
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.JavaScriptFunction', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('JavaScriptFunction')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
135
packages/noodl-runtime/src/nodes/std-library/stringformat.js
Normal file
135
packages/noodl-runtime/src/nodes/std-library/stringformat.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const StringFormatDefinition = {
|
||||
name: 'String Format',
|
||||
docs: 'https://docs.noodl.net/nodes/string-manipulation/string-format',
|
||||
category: 'String Manipulation',
|
||||
initialize() {
|
||||
const internal = this._internal;
|
||||
internal.format = '';
|
||||
internal.cachedResult = '';
|
||||
internal.resultDirty = false;
|
||||
internal.inputValues = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this.formatValue();
|
||||
},
|
||||
inputs: {
|
||||
format: {
|
||||
type: { name: 'string', multiline: true },
|
||||
displayName: 'Format',
|
||||
set(value) {
|
||||
if (this._internal.format === value) return;
|
||||
|
||||
this._internal.format = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.scheduleFormat();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
formatted: {
|
||||
type: 'string',
|
||||
displayName: 'Formatted',
|
||||
get() {
|
||||
return this.formatValue();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatValue() {
|
||||
var internal = this._internal;
|
||||
|
||||
if (internal.resultDirty) {
|
||||
var formatted = internal.format;
|
||||
|
||||
var matches = internal.format.match(/\{[A-Za-z0-9_]*\}/g);
|
||||
var inputs = [];
|
||||
if (matches) {
|
||||
inputs = matches.map(function (name) {
|
||||
return name.replace('{', '').replace('}', '');
|
||||
});
|
||||
}
|
||||
|
||||
inputs.forEach(function (name) {
|
||||
var v = internal.inputValues[name];
|
||||
formatted = formatted.replace('{' + name + '}', v !== undefined ? v : '');
|
||||
});
|
||||
|
||||
internal.cachedResult = formatted;
|
||||
internal.resultDirty = false;
|
||||
}
|
||||
|
||||
return internal.cachedResult;
|
||||
},
|
||||
registerInputIfNeeded(name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
},
|
||||
scheduleFormat() {
|
||||
if (this.formatScheduled) return;
|
||||
|
||||
this.formatScheduled = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.formatValue();
|
||||
this.flagOutputDirty('formatted');
|
||||
this.formatScheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
if (this._internal.inputValues[name] === value) return;
|
||||
|
||||
this._internal.inputValues[name] = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.scheduleFormat();
|
||||
}
|
||||
|
||||
function updatePorts(id, format, editorConnection) {
|
||||
var inputs = format.match(/\{[A-Za-z0-9_]*\}/g) || [];
|
||||
var portsNames = inputs.map(function (def) {
|
||||
return def.replace('{', '').replace('}', '');
|
||||
});
|
||||
|
||||
var ports = portsNames
|
||||
//get unique names
|
||||
.filter(function (value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
})
|
||||
//and map names to ports
|
||||
.map(function (name) {
|
||||
return {
|
||||
name: name,
|
||||
type: 'string',
|
||||
plug: 'input'
|
||||
};
|
||||
});
|
||||
|
||||
editorConnection.sendDynamicPorts(id, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: StringFormatDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.String Format', function (node) {
|
||||
if (node.parameters.format) {
|
||||
updatePorts(node.id, node.parameters.format, context.editorConnection);
|
||||
}
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'format') {
|
||||
updatePorts(node.id, node.parameters.format, context.editorConnection);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
94
packages/noodl-runtime/src/nodes/std-library/stringmapper.js
Normal file
94
packages/noodl-runtime/src/nodes/std-library/stringmapper.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const StringMapperNode = {
|
||||
name: 'String Mapper',
|
||||
docs: 'https://docs.noodl.net/nodes/string-manipulation/string-mapper',
|
||||
category: 'Utilities',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
this._internal.mappings = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.mappedString;
|
||||
},
|
||||
numberedInputs: {
|
||||
input: {
|
||||
type: 'string',
|
||||
displayPrefix: 'Input',
|
||||
group: 'Inputs',
|
||||
index: 10,
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
value = value === undefined ? '' : value.toString();
|
||||
this._internal.inputs[index] = value;
|
||||
this.scheduleMapping();
|
||||
};
|
||||
}
|
||||
},
|
||||
output: {
|
||||
type: 'string',
|
||||
displayPrefix: 'Mapping',
|
||||
index: 1001,
|
||||
group: 'Mappings',
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
value = value === undefined ? '' : value.toString();
|
||||
this._internal.mappings[index] = value;
|
||||
this.scheduleMapping();
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
inputString: {
|
||||
type: {
|
||||
name: 'string'
|
||||
},
|
||||
index: 1,
|
||||
displayName: 'Input String',
|
||||
set: function (value) {
|
||||
this._internal.currentInputString = value !== undefined ? value.toString() : undefined;
|
||||
this.scheduleMapping();
|
||||
}
|
||||
},
|
||||
defaultMapping: {
|
||||
type: 'string',
|
||||
displayName: 'Default',
|
||||
index: 1000,
|
||||
group: 'Mappings',
|
||||
set: function (value) {
|
||||
this._internal.defaultMapping = value;
|
||||
this.scheduleMapping();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
mappedString: {
|
||||
type: 'string',
|
||||
displayName: 'Mapped String',
|
||||
group: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.mappedString;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
doMapping: function () {
|
||||
this._internal.hasScheduledFetch = false;
|
||||
var idx = this._internal.inputs.indexOf(this._internal.currentInputString);
|
||||
if (idx !== -1) this._internal.mappedString = this._internal.mappings[idx];
|
||||
else this._internal.mappedString = this._internal.defaultMapping;
|
||||
|
||||
this.flagOutputDirty('mappedString');
|
||||
},
|
||||
scheduleMapping: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledFetch) {
|
||||
internal.hasScheduledFetch = true;
|
||||
this.scheduleAfterInputsHaveUpdated(this.doMapping.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: StringMapperNode
|
||||
};
|
||||
76
packages/noodl-runtime/src/nodes/std-library/substring.js
Normal file
76
packages/noodl-runtime/src/nodes/std-library/substring.js
Normal file
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
const SubStringNode = {
|
||||
name: 'Substring',
|
||||
docs: 'https://docs.noodl.net/nodes/string-manipulation/substring',
|
||||
category: 'String Manipulation',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.startIndex = 0;
|
||||
internal.endIndex = -1;
|
||||
internal.cachedResult = '';
|
||||
internal.inputString = '';
|
||||
internal.resultDirty = false;
|
||||
},
|
||||
inputs: {
|
||||
start: {
|
||||
type: 'number',
|
||||
displayName: 'Start',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.startIndex = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
},
|
||||
end: {
|
||||
type: 'number',
|
||||
displayName: 'End',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.endIndex = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
},
|
||||
string: {
|
||||
type: {
|
||||
name: 'string'
|
||||
},
|
||||
displayName: 'String',
|
||||
default: '',
|
||||
set: function (value) {
|
||||
value = value.toString();
|
||||
this._internal.inputString = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'string',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
if (internal.resultDirty) {
|
||||
if (internal.endIndex === -1) {
|
||||
internal.cachedResult = internal.inputString.substr(internal.startIndex);
|
||||
} else {
|
||||
internal.cachedResult = internal.inputString.substr(
|
||||
internal.startIndex,
|
||||
internal.endIndex - internal.startIndex
|
||||
);
|
||||
}
|
||||
internal.resultDirty = false;
|
||||
}
|
||||
return internal.cachedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: SubStringNode
|
||||
};
|
||||
41
packages/noodl-runtime/src/nodes/std-library/uniqueid.js
Normal file
41
packages/noodl-runtime/src/nodes/std-library/uniqueid.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const Model = require('../../model');
|
||||
|
||||
const UniqueIdNode = {
|
||||
name: 'Unique Id',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/unique-id',
|
||||
category: 'String Manipulation',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.guid = Model.guid();
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.guid;
|
||||
},
|
||||
inputs: {
|
||||
new: {
|
||||
displayName: 'New',
|
||||
valueChangedToTrue: function () {
|
||||
var internal = this._internal;
|
||||
internal.guid = Model.guid();
|
||||
this.flagOutputDirty('guid');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
guid: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
getter: function () {
|
||||
var internal = this._internal;
|
||||
return internal.guid;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: UniqueIdNode
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
'use strict';
|
||||
|
||||
const NoodlRuntime = require('../../../../noodl-runtime');
|
||||
|
||||
var SetUserPropertiesNodeDefinition = {
|
||||
name: 'net.noodl.user.SetUserProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/set-user-properties',
|
||||
displayNodeName: 'Set User Properties',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.userProperties = {};
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleStore();
|
||||
}
|
||||
},
|
||||
email: {
|
||||
displayName: 'Email',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.email = value;
|
||||
}
|
||||
},
|
||||
username: {
|
||||
displayName: 'Username',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.username = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'user-set-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'user-set-warning');
|
||||
}
|
||||
},
|
||||
scheduleStore: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.storeScheduled === true) return;
|
||||
this.storeScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.storeScheduled = false;
|
||||
|
||||
const UserService = NoodlRuntime.Services.UserService;
|
||||
UserService.forScope(this.nodeScope.modelScope).setUserProperties({
|
||||
email: this._internal.email,
|
||||
username: this._internal.username,
|
||||
properties: internal.userProperties,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
setUserProperty: function (name, value) {
|
||||
this._internal.userProperties[name] = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setUserProperty.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, systemCollections) {
|
||||
var ports = [];
|
||||
|
||||
if (systemCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = systemCollections.find((c) => c.name === '_User');
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
|
||||
const _ignoreKeys =
|
||||
typeof _noodl_cloud_runtime_version === 'undefined'
|
||||
? ['authData', 'createdAt', 'updatedAt', 'email', 'username', 'emailVerified', 'password']
|
||||
: ['authData', 'createdAt', 'updatedAt', 'email', 'username'];
|
||||
|
||||
for (var key in props) {
|
||||
if (_ignoreKeys.indexOf(key) !== -1) continue;
|
||||
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
if (p.type === 'Relation') {
|
||||
} else {
|
||||
// Other schema type ports
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: SetUserPropertiesNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.net.noodl.user.SetUserProperties', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('net.noodl.user.SetUserProperties')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
338
packages/noodl-runtime/src/nodes/std-library/user/user.js
Normal file
338
packages/noodl-runtime/src/nodes/std-library/user/user.js
Normal file
@@ -0,0 +1,338 @@
|
||||
'use strict';
|
||||
|
||||
const NoodlRuntime = require('../../../../noodl-runtime');
|
||||
const { Node } = require('../../../../noodl-runtime');
|
||||
|
||||
var UserNodeDefinition = {
|
||||
name: 'net.noodl.user.User',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/user-node',
|
||||
displayNodeName: 'User',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
this._internal.onModelChangedCallback = function (args) {
|
||||
if (_this.isInputConnected('fetch')) return;
|
||||
|
||||
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
|
||||
|
||||
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
|
||||
|
||||
_this.sendSignalOnOutput('changed');
|
||||
};
|
||||
|
||||
const userService = NoodlRuntime.Services.UserService.forScope(this.nodeScope.modelScope);
|
||||
|
||||
this.setUserModel(userService.current);
|
||||
userService.on('loggedIn', () => {
|
||||
this.setUserModel(userService.current);
|
||||
|
||||
if (this.hasOutput('loggedIn')) this.sendSignalOnOutput('loggedIn');
|
||||
});
|
||||
|
||||
userService.on('sessionGained', () => {
|
||||
this.setUserModel(userService.current);
|
||||
});
|
||||
|
||||
userService.on('loggedOut', () => {
|
||||
this.setUserModel(undefined);
|
||||
if (this.hasOutput('loggedOut')) this.sendSignalOnOutput('loggedOut');
|
||||
});
|
||||
|
||||
userService.on('sessionLost', () => {
|
||||
this.setUserModel(undefined);
|
||||
if (this.hasOutput('sessionLost')) this.sendSignalOnOutput('sessionLost');
|
||||
});
|
||||
},
|
||||
getInspectInfo() {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Model]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: this._internal.model.data }
|
||||
];
|
||||
},
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined ? this._internal.model.getId() : undefined;
|
||||
}
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
group: 'Events'
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
displayName: 'Username',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined ? this._internal.model.get('username') : undefined;
|
||||
}
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
displayName: 'Email',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined ? this._internal.model.get('email') : undefined;
|
||||
}
|
||||
},
|
||||
authenticated: {
|
||||
type: 'boolean',
|
||||
displayName: 'Authenticated',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined;
|
||||
}
|
||||
}
|
||||
/* loggedIn:{
|
||||
type:'signal',
|
||||
displayName:'Logged In',
|
||||
group:'Events'
|
||||
},
|
||||
loggedOut:{
|
||||
type:'signal',
|
||||
displayName:'Logged Out',
|
||||
group:'Events'
|
||||
},
|
||||
sessionLost:{
|
||||
type:'signal',
|
||||
displayName:'Session Lost',
|
||||
group:'Events'
|
||||
}, */
|
||||
},
|
||||
inputs: {
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
},
|
||||
scheduleOnce: function (type, cb) {
|
||||
const _this = this;
|
||||
const _type = 'hasScheduled' + type;
|
||||
if (this._internal[_type]) return;
|
||||
this._internal[_type] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this._internal[_type] = false;
|
||||
cb();
|
||||
});
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'user-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'user-warning');
|
||||
}
|
||||
},
|
||||
setUserModel(model) {
|
||||
const internal = this._internal;
|
||||
|
||||
if (internal.model !== model) {
|
||||
// Check if we need to change model
|
||||
if (internal.model)
|
||||
// Remove old listener if existing
|
||||
internal.model.off('change', internal.onModelChangedCallback);
|
||||
|
||||
internal.model = model;
|
||||
if (model) model.on('change', internal.onModelChangedCallback);
|
||||
}
|
||||
this.flagOutputDirty('id');
|
||||
this.flagOutputDirty('authenticated');
|
||||
this.flagOutputDirty('email');
|
||||
this.flagOutputDirty('username');
|
||||
|
||||
// Notify all properties changed
|
||||
if (model)
|
||||
for (var key in model.data) {
|
||||
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('Fetch', () => {
|
||||
const userService = NoodlRuntime.Services.UserService.forScope(this.nodeScope.modelScope);
|
||||
userService.fetchCurrentUser({
|
||||
success: (response) => {
|
||||
this.setUserModel(userService.current);
|
||||
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
error: (err) => {
|
||||
this.setError(err || 'Failed to fetch.');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'loggedOut' || name === 'loggedIn' || name === 'sessionLost') {
|
||||
this.registerOutput(name, {
|
||||
getter: () => {} /* No getter needed, signal */
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerOutput(name, {
|
||||
getter: this.getUserProperty.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
},
|
||||
getUserProperty: function (name) {
|
||||
return this._internal.model !== undefined ? this._internal.model.get(name) : undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, systemCollections) {
|
||||
var ports = [];
|
||||
|
||||
if (systemCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = systemCollections.find((c) => c.name === '_User');
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
const _ignoreKeys = ['authData', 'password', 'username', 'email'];
|
||||
for (var key in props) {
|
||||
if (_ignoreKeys.indexOf(key) !== -1) continue;
|
||||
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
if (p.type === 'Relation') {
|
||||
} else {
|
||||
// Other schema type ports
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: key + ' Changed',
|
||||
name: 'changed-' + key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// On the client we have some extra outputs
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
name: 'loggedIn',
|
||||
type: 'signal',
|
||||
displayName: 'Logged In',
|
||||
group: 'Events'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
name: 'loggedOut',
|
||||
type: 'signal',
|
||||
displayName: 'Logged Out',
|
||||
group: 'Events'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
name: 'sessionLost',
|
||||
type: 'signal',
|
||||
displayName: 'Session Lost',
|
||||
group: 'Events'
|
||||
});
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: UserNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.net.noodl.user.User', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('net.noodl.user.User')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const VariableBase = require('./variablebase');
|
||||
|
||||
const BooleanNode = VariableBase.createDefinition({
|
||||
name: 'Boolean',
|
||||
docs: 'https://docs.noodl.net/nodes/data/boolean',
|
||||
startValue: false,
|
||||
type: {
|
||||
name: 'boolean'
|
||||
},
|
||||
cast: function (value) {
|
||||
return Boolean(value);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
node: BooleanNode
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const VariableBase = require('./variablebase');
|
||||
|
||||
const NumberNode = VariableBase.createDefinition({
|
||||
name: 'Number',
|
||||
docs: 'https://docs.noodl.net/nodes/data/number',
|
||||
startValue: 0,
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'value'
|
||||
},
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
cast: function (value) {
|
||||
return Number(value);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
node: NumberNode
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const VariableBase = require('./variablebase');
|
||||
const { NodeDefinition } = require('../../../../noodl-runtime');
|
||||
|
||||
const StringNode = VariableBase.createDefinition({
|
||||
name: 'String',
|
||||
docs: 'https://docs.noodl.net/nodes/data/string',
|
||||
shortDesc: 'Contains a string (text).',
|
||||
startValue: '',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'value'
|
||||
},
|
||||
type: {
|
||||
name: 'string'
|
||||
},
|
||||
cast: function (value) {
|
||||
return String(value);
|
||||
},
|
||||
onChanged: function () {
|
||||
this.flagOutputDirty('length');
|
||||
}
|
||||
});
|
||||
|
||||
NodeDefinition.extend(StringNode, {
|
||||
usePortAsLabel: 'value',
|
||||
portLabelTruncationMode: 'length',
|
||||
outputs: {
|
||||
length: {
|
||||
type: 'number',
|
||||
displayName: 'Length',
|
||||
getter: function () {
|
||||
return this._internal.currentValue.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
node: StringNode
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
function createDefinition(args) {
|
||||
return {
|
||||
name: args.name,
|
||||
docs: args.docs,
|
||||
shortDesc: args.shortDesc,
|
||||
nodeDoubleClickAction: args.nodeDoubleClickAction,
|
||||
category: 'Variables',
|
||||
initialize: function () {
|
||||
this._internal.currentValue = args.startValue;
|
||||
this._internal.latestValue = 0;
|
||||
},
|
||||
getInspectInfo() {
|
||||
const type = args.type.name === 'color' ? 'color' : 'text';
|
||||
return [{ type, value: this._internal.currentValue }];
|
||||
},
|
||||
inputs: {
|
||||
value: {
|
||||
type: args.type,
|
||||
displayName: 'Value',
|
||||
default: args.startValue,
|
||||
set: function (value) {
|
||||
if (this.isInputConnected('saveValue') === false) {
|
||||
this.setValueTo(value);
|
||||
} else {
|
||||
this._internal.latestValue = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
saveValue: {
|
||||
displayName: 'Set',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
this.setValueTo(this._internal.latestValue);
|
||||
this.sendSignalOnOutput('stored');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
savedValue: {
|
||||
type: args.type.name,
|
||||
displayName: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.currentValue;
|
||||
}
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed'
|
||||
},
|
||||
stored: {
|
||||
type: 'signal',
|
||||
displayName: 'Stored'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setValueTo: function (value) {
|
||||
value = args.cast(value);
|
||||
const changed = this._internal.currentValue !== value;
|
||||
this._internal.currentValue = value;
|
||||
|
||||
if (changed) {
|
||||
this.flagOutputDirty('savedValue');
|
||||
this.sendSignalOnOutput('changed');
|
||||
args.onChanged && args.onChanged.call(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDefinition: createDefinition
|
||||
};
|
||||
461
packages/noodl-runtime/src/nodescope.js
Normal file
461
packages/noodl-runtime/src/nodescope.js
Normal file
@@ -0,0 +1,461 @@
|
||||
'use strict';
|
||||
|
||||
const guid = require('./guid');
|
||||
|
||||
function NodeScope(context, componentOwner) {
|
||||
this.context = context;
|
||||
this.nodes = {};
|
||||
this.componentOwner = componentOwner; //Component Instance that owns this NodeScope
|
||||
this.componentInstanceChildren = {};
|
||||
}
|
||||
|
||||
function verifyData(data, requiredKeys) {
|
||||
requiredKeys.forEach(function (key) {
|
||||
if (!data[key]) {
|
||||
throw new Error('Missing ' + key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
NodeScope.prototype.addConnection = function (connectionData) {
|
||||
try {
|
||||
verifyData(connectionData, ['sourceId', 'sourcePort', 'targetId', 'targetPort']);
|
||||
} catch (e) {
|
||||
throw new Error('Error in connection: ' + e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
var sourceNode = this.getNodeWithId(connectionData.sourceId),
|
||||
targetNode = this.getNodeWithId(connectionData.targetId);
|
||||
|
||||
targetNode.registerInputIfNeeded(connectionData.targetPort);
|
||||
sourceNode.registerOutputIfNeeded(connectionData.sourcePort);
|
||||
targetNode.connectInput(connectionData.targetPort, sourceNode, connectionData.sourcePort);
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
NodeScope.prototype.setNodeParameters = function (node, nodeModel) {
|
||||
const variant = this.context.variants.getVariant(nodeModel.type, nodeModel.variant);
|
||||
|
||||
if (variant) {
|
||||
//apply the variant (this will also apply the parameters)
|
||||
node.setVariant(variant);
|
||||
} else {
|
||||
const parameters = nodeModel.parameters;
|
||||
|
||||
var inputNames = Object.keys(parameters);
|
||||
|
||||
if (this.context.nodeRegister.hasNode(node.name)) {
|
||||
var metadata = this.context.nodeRegister.getNodeMetadata(node.name);
|
||||
inputNames.sort(function (a, b) {
|
||||
var inputA = metadata.inputs[a];
|
||||
var inputB = metadata.inputs[b];
|
||||
return (inputB ? inputB.inputPriority : 0) - (inputA ? inputA.inputPriority : 0);
|
||||
});
|
||||
}
|
||||
|
||||
inputNames.forEach((inputName) => {
|
||||
node.registerInputIfNeeded(inputName);
|
||||
|
||||
//protect against obsolete parameters
|
||||
if (node.hasInput(inputName) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.queueInput(inputName, parameters[inputName]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
NodeScope.prototype.createNodeFromModel = async function (nodeModel, updateOnDirtyFlagging) {
|
||||
if (nodeModel.type === 'Component Children') {
|
||||
if (nodeModel.parent) {
|
||||
var parentInstance = this.getNodeWithId(nodeModel.parent.id);
|
||||
this.componentOwner.setChildRoot(parentInstance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var node;
|
||||
try {
|
||||
node = await this.createNode(nodeModel.type, nodeModel.id);
|
||||
node.updateOnDirtyFlagging = updateOnDirtyFlagging === false ? false : true;
|
||||
node.setNodeModel(nodeModel);
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('nodescope')) {
|
||||
this.context.editorConnection.sendWarning(this.componentOwner.name, nodeModel.id, 'nodelibrary-unknown-node', {
|
||||
message: e.message,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeModel.variant && node.setVariant) node.setVariant(nodeModel.variant);
|
||||
this.setNodeParameters(node, nodeModel);
|
||||
|
||||
if (nodeModel.parent) {
|
||||
this.insertNodeInTree(node, nodeModel);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
NodeScope.prototype.insertNodeInTree = function (nodeInstance, nodeModel) {
|
||||
var parentInstance = this.getNodeWithId(nodeModel.parent.id);
|
||||
var childIndex = nodeModel.parent.children.indexOf(nodeModel);
|
||||
|
||||
if (!parentInstance.addChild) {
|
||||
throw new Error(
|
||||
'Node ' + parentInstance.id + ' of type ' + parentInstance.constructor.name + " can't have children"
|
||||
);
|
||||
}
|
||||
|
||||
parentInstance.addChild(nodeInstance, childIndex);
|
||||
};
|
||||
|
||||
NodeScope.prototype.getNodeWithId = function (id) {
|
||||
if (this.nodes.hasOwnProperty(id) === false) {
|
||||
throw new Error('Unknown node id ' + id);
|
||||
}
|
||||
return this.nodes[id];
|
||||
};
|
||||
|
||||
NodeScope.prototype.hasNodeWithId = function (id) {
|
||||
return this.nodes.hasOwnProperty(id);
|
||||
};
|
||||
|
||||
NodeScope.prototype.createPrimitiveNode = function (name, id, extraProps) {
|
||||
if (!id) id = guid();
|
||||
|
||||
if (this.nodes.hasOwnProperty(id)) {
|
||||
throw Error('duplicate id ' + id);
|
||||
}
|
||||
|
||||
const node = this.context.nodeRegister.createNode(name, id, this);
|
||||
if (extraProps) {
|
||||
for (const prop in extraProps) {
|
||||
node[prop] = extraProps[prop];
|
||||
}
|
||||
}
|
||||
|
||||
this.nodes[id] = node;
|
||||
return node;
|
||||
};
|
||||
|
||||
NodeScope.prototype.createNode = async function (name, id, extraProps) {
|
||||
if (!id) id = guid();
|
||||
|
||||
if (this.nodes.hasOwnProperty(id)) {
|
||||
throw Error('duplicate id ' + id);
|
||||
}
|
||||
|
||||
let node;
|
||||
|
||||
if (this.context.nodeRegister.hasNode(name)) {
|
||||
node = this.context.nodeRegister.createNode(name, id, this);
|
||||
if (extraProps) {
|
||||
for (const prop in extraProps) {
|
||||
node[prop] = extraProps[prop];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node = await this.context.createComponentInstanceNode(name, id, this, extraProps);
|
||||
this.componentInstanceChildren[id] = node;
|
||||
}
|
||||
|
||||
this.nodes[id] = node;
|
||||
return node;
|
||||
};
|
||||
|
||||
NodeScope.prototype.getNodesWithIdRecursive = function (id) {
|
||||
var ComponentInstanceNode = require('./nodes/componentinstance');
|
||||
|
||||
function findNodesWithIdRec(scope, id, result) {
|
||||
if (scope.nodes.hasOwnProperty(id)) {
|
||||
result.push(scope.nodes[id]);
|
||||
}
|
||||
|
||||
var componentIds = Object.keys(scope.nodes).filter(function (nodeId) {
|
||||
return scope.nodes[nodeId] instanceof ComponentInstanceNode;
|
||||
});
|
||||
|
||||
componentIds.forEach(function (componentId) {
|
||||
findNodesWithIdRec(scope.nodes[componentId].nodeScope, id, result);
|
||||
});
|
||||
}
|
||||
|
||||
var result = [];
|
||||
findNodesWithIdRec(this, id, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
NodeScope.prototype.getNodesWithType = function (name) {
|
||||
var self = this;
|
||||
var ids = Object.keys(this.nodes).filter(function (id) {
|
||||
return self.nodes[id].name === name;
|
||||
});
|
||||
return ids.map(function (id) {
|
||||
return self.nodes[id];
|
||||
});
|
||||
};
|
||||
|
||||
NodeScope.prototype.getNodesWithTypeRecursive = function (name) {
|
||||
var ComponentInstanceNode = require('./nodes/componentinstance');
|
||||
|
||||
var self = this;
|
||||
function findNodesWithTypeRec() {
|
||||
result = result.concat(self.getNodesWithType(name));
|
||||
|
||||
var componentIds = Object.keys(self.nodes).filter(function (nodeId) {
|
||||
return self.nodes[nodeId] instanceof ComponentInstanceNode;
|
||||
});
|
||||
|
||||
componentIds.forEach(function (componentId) {
|
||||
var res = self.nodes[componentId].nodeScope.getNodesWithTypeRecursive(name);
|
||||
result = result.concat(res);
|
||||
});
|
||||
}
|
||||
|
||||
var result = [];
|
||||
findNodesWithTypeRec(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
NodeScope.prototype.getAllNodesRecursive = function () {
|
||||
var ComponentInstanceNode = require('./nodes/componentinstance');
|
||||
|
||||
let result = [];
|
||||
|
||||
const getAllNodesRec = () => {
|
||||
result = result.concat(Object.values(this.nodes));
|
||||
|
||||
var componentIds = Object.keys(this.nodes).filter((nodeId) => {
|
||||
return this.nodes[nodeId] instanceof ComponentInstanceNode;
|
||||
});
|
||||
|
||||
componentIds.forEach((componentId) => {
|
||||
var res = this.nodes[componentId].nodeScope.getAllNodesRecursive();
|
||||
result = result.concat(res);
|
||||
});
|
||||
};
|
||||
|
||||
getAllNodesRec(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
NodeScope.prototype.getAllNodesWithVariantRecursive = function (variant) {
|
||||
const nodes = this.getAllNodesRecursive();
|
||||
return nodes.filter((node) => node.variant === variant);
|
||||
};
|
||||
|
||||
NodeScope.prototype.onNodeModelRemoved = function (nodeModel) {
|
||||
var nodeInstance = this.getNodeWithId(nodeModel.id);
|
||||
|
||||
if (nodeModel.parent) {
|
||||
var parentInstance = this.getNodeWithId(nodeModel.parent.id);
|
||||
parentInstance.removeChild(nodeInstance);
|
||||
}
|
||||
|
||||
nodeInstance._onNodeDeleted();
|
||||
delete this.nodes[nodeInstance.id];
|
||||
delete this.componentInstanceChildren[nodeInstance.id];
|
||||
};
|
||||
|
||||
NodeScope.prototype.removeConnection = function (connectionModel) {
|
||||
var targetNode = this.getNodeWithId(connectionModel.targetId);
|
||||
targetNode.removeInputConnection(connectionModel.targetPort, connectionModel.sourceId, connectionModel.sourcePort);
|
||||
};
|
||||
|
||||
NodeScope.prototype.setComponentModel = async function (componentModel) {
|
||||
this.componentModel = componentModel;
|
||||
|
||||
const nodes = [];
|
||||
|
||||
//create all nodes
|
||||
for (const nodeModel of componentModel.getAllNodes()) {
|
||||
const node = await this.createNodeFromModel(nodeModel, false);
|
||||
if (node) nodes.push(node);
|
||||
}
|
||||
|
||||
componentModel.getAllConnections().forEach((conn) => this.addConnection(conn));
|
||||
|
||||
//now that all nodes and connections are setup, trigger the dirty flagging so nodes can run with all the connections in place
|
||||
nodes.forEach((node) => (node.updateOnDirtyFlagging = true));
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (node._dirty) {
|
||||
node._performDirtyUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
componentModel.on('connectionAdded', (conn) => this.addConnection(conn), this);
|
||||
componentModel.on('connectionRemoved', this.removeConnection, this);
|
||||
componentModel.on('nodeAdded', this.createNodeFromModel, this);
|
||||
|
||||
var self = this;
|
||||
componentModel.on(
|
||||
'nodeParentWillBeRemoved',
|
||||
function (nodeModel) {
|
||||
if (nodeModel.type === 'Component Children') {
|
||||
if (nodeModel.parent) {
|
||||
this.componentOwner.setChildRoot(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInstance = self.getNodeWithId(nodeModel.id);
|
||||
if (nodeInstance.parent) {
|
||||
nodeInstance.parent.removeChild(nodeInstance);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
componentModel.on(
|
||||
'nodeParentUpdated',
|
||||
function (nodeModel) {
|
||||
if (nodeModel.type === 'Component Children') {
|
||||
var parentInstance = this.getNodeWithId(nodeModel.parent.id);
|
||||
this.componentOwner.setChildRoot(parentInstance);
|
||||
} else {
|
||||
var nodeInstance = self.getNodeWithId(nodeModel.id);
|
||||
self.insertNodeInTree(nodeInstance, nodeModel);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
componentModel.on(
|
||||
'nodeRemoved',
|
||||
function (nodeModel) {
|
||||
if (nodeModel.type !== 'Component Children') {
|
||||
self.onNodeModelRemoved(nodeModel);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
for (const id in this.nodes) {
|
||||
const node = this.nodes[id];
|
||||
node.nodeScopeDidInitialize && node.nodeScopeDidInitialize();
|
||||
}
|
||||
};
|
||||
|
||||
NodeScope.prototype.reset = function () {
|
||||
if (this.componentModel) {
|
||||
this.componentModel.removeListenersWithRef(this);
|
||||
this.componentModel = undefined;
|
||||
}
|
||||
|
||||
Object.keys(this.nodes).forEach((id) => {
|
||||
if (this.nodes.hasOwnProperty(id)) {
|
||||
this.deleteNode(this.nodes[id]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
NodeScope.prototype.deleteNode = function (nodeInstance) {
|
||||
if (this.nodes.hasOwnProperty(nodeInstance.id) === false) {
|
||||
console.error("Node doesn't belong to this scope", nodeInstance.id, nodeInstance.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeInstance.parent) {
|
||||
nodeInstance.parent.removeChild(nodeInstance);
|
||||
}
|
||||
|
||||
//depth first
|
||||
if (nodeInstance.getChildren) {
|
||||
nodeInstance.getChildren().forEach((child) => {
|
||||
nodeInstance.removeChild(child);
|
||||
//the child might be created in a different scope
|
||||
//if the child is a component instance, we want its parent scope, not the inner scope
|
||||
const nodeScope = child.parentNodeScope || child.nodeScope;
|
||||
nodeScope.deleteNode(child);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.componentModel) {
|
||||
const connectionFrom = this.componentModel.getConnectionsFrom(nodeInstance.id);
|
||||
const connectionTo = this.componentModel.getConnectionsTo(nodeInstance.id);
|
||||
|
||||
connectionFrom.concat(connectionTo).forEach((connection) => {
|
||||
if (this.nodes.hasOwnProperty(connection.targetId) && this.nodes.hasOwnProperty(connection.sourceId)) {
|
||||
this.removeConnection(connection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nodeInstance._onNodeDeleted();
|
||||
delete this.nodes[nodeInstance.id];
|
||||
delete this.componentInstanceChildren[nodeInstance.id]; //in case this is a component
|
||||
};
|
||||
|
||||
NodeScope.prototype.sendEventFromThisScope = function (eventName, data, propagation, sendEventInThisScope, _exclude) {
|
||||
if (sendEventInThisScope) {
|
||||
var eventReceivers = this.getNodesWithType('Event Receiver').filter(function (eventReceiver) {
|
||||
return eventReceiver.getChannelName() === eventName;
|
||||
});
|
||||
|
||||
for (var i = 0; i < eventReceivers.length; i++) {
|
||||
var consumed = eventReceivers[i].handleEvent(data);
|
||||
if (consumed) return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (propagation === 'parent' && this.componentOwner.parentNodeScope) {
|
||||
// Send event to parent scope
|
||||
//either the scope of the visual parent if there is one, otherwise the parent component
|
||||
const parentNodeScope = this.componentOwner.parent
|
||||
? this.componentOwner.parent.nodeScope
|
||||
: this.componentOwner.parentNodeScope;
|
||||
if (!parentNodeScope) return;
|
||||
parentNodeScope.sendEventFromThisScope(eventName, data, propagation, true);
|
||||
} else if (propagation === 'children') {
|
||||
// Send event to all child scopes
|
||||
var nodes = this.nodes;
|
||||
for (var nodeId in nodes) {
|
||||
var children = nodes[nodeId].children;
|
||||
if (children)
|
||||
children.forEach((child) => {
|
||||
if (child.name && this.context.hasComponentModelWithName(child.name)) {
|
||||
// This is a component instance child
|
||||
var consumed = child.nodeScope.sendEventFromThisScope(eventName, data, propagation, true);
|
||||
if (consumed) return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (propagation === 'siblings') {
|
||||
// Send event to all siblings, that is all children of the parent scope except this scope
|
||||
let parentNodeScope;
|
||||
if (this.componentOwner.parent) {
|
||||
parentNodeScope = this.componentOwner.parent.nodeScope;
|
||||
} else {
|
||||
parentNodeScope = this.componentOwner.parentNodeScope;
|
||||
}
|
||||
|
||||
if (!parentNodeScope) return;
|
||||
|
||||
var nodes = parentNodeScope.nodes;
|
||||
for (var nodeId in nodes) {
|
||||
var children = nodes[nodeId].children;
|
||||
if (children) {
|
||||
var _c = children.filter(
|
||||
(child) => child.name && this.context.hasComponentModelWithName(child.name) && child.nodeScope !== this
|
||||
);
|
||||
_c.forEach((child) => {
|
||||
var consumed = child.nodeScope.sendEventFromThisScope(eventName, data, null, true);
|
||||
if (consumed) return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports = NodeScope;
|
||||
132
packages/noodl-runtime/src/outputproperty.js
Normal file
132
packages/noodl-runtime/src/outputproperty.js
Normal file
@@ -0,0 +1,132 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @class OutputProperty
|
||||
*
|
||||
* @param args Arguments
|
||||
* @param {Function} args.getter Function that returns the value
|
||||
* @param {Node} args.owner Port owner
|
||||
* @param {string} args.type Output value type
|
||||
* @constructor
|
||||
*/
|
||||
function OutputProperty(args) {
|
||||
|
||||
if(!args.owner) {
|
||||
throw new Error("Owner must be set");
|
||||
}
|
||||
this.getter = args.getter;
|
||||
this.connections = [];
|
||||
this.owner = args.owner;
|
||||
this.name = args.name;
|
||||
this.onFirstConnectionAdded = args.onFirstConnectionAdded;
|
||||
this.onLastConnectionRemoved = args.onLastConnectionRemoved;
|
||||
|
||||
this._id = undefined;
|
||||
}
|
||||
|
||||
Object.defineProperties(OutputProperty.prototype, {
|
||||
/**
|
||||
* Gets the current value
|
||||
* @name OutputProperty#value
|
||||
* @readonly
|
||||
*/
|
||||
value: {
|
||||
get: function() {
|
||||
return this.getter.call(this.owner);
|
||||
}
|
||||
},
|
||||
|
||||
id: {
|
||||
get: function() {
|
||||
if(!this._id) {
|
||||
this._id = this.owner.id + this.name;
|
||||
}
|
||||
return this._id;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a connection to propagate dirtiness
|
||||
* Also records which input port it's connected to so nodes like
|
||||
* Animation can set it's implicit start value
|
||||
* @name OutputProperty#registerConnection
|
||||
* @readonly
|
||||
*/
|
||||
registerConnection: {
|
||||
value: function(node, inputPortName) {
|
||||
this.connections.push({
|
||||
node: node,
|
||||
inputPortName: inputPortName
|
||||
});
|
||||
|
||||
if(this.connections.length === 1 && this.onFirstConnectionAdded) {
|
||||
this.onFirstConnectionAdded.call(this.owner);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deregisters a connection to a specific input port
|
||||
* @name OutputProperty#deregisterConnection
|
||||
* @readonly
|
||||
*/
|
||||
deregisterConnection: {
|
||||
value: function(node, inputPortName) {
|
||||
for(var i=0; i<this.connections.length; i++) {
|
||||
var connection = this.connections[i];
|
||||
if(connection.node === node && connection.inputPortName === inputPortName) {
|
||||
this.connections.splice(i,1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.connections.length === 0 && this.onLastConnectionRemoved) {
|
||||
this.onLastConnectionRemoved.call(this.owner);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets dirty flag of all nodes that depend on this output port
|
||||
* @name OutputProperty#flagDependeesDirty
|
||||
* @readonly
|
||||
*/
|
||||
flagDependeesDirty: {
|
||||
value: function(value) {
|
||||
for(var i= 0, len=this.connections.length; i<len; i++) {
|
||||
this.connections[i].node.flagDirty();
|
||||
}
|
||||
}
|
||||
},
|
||||
sendValue: {
|
||||
value: function(value) {
|
||||
if(this._lastUpdateIteration !== this.owner._updatedAtIteration) {
|
||||
this._lastUpdateIteration = this.owner._updatedAtIteration;
|
||||
this.valuesSendThisIteration = 0;
|
||||
}
|
||||
else {
|
||||
this.valuesSendThisIteration++;
|
||||
}
|
||||
|
||||
if(this.valuesSendThisIteration > 500) {
|
||||
//this will make the owner send a warning and stop its update
|
||||
this.owner._cyclicLoop = true;
|
||||
}
|
||||
|
||||
for(var i= 0, len=this.connections.length; i<len; i++) {
|
||||
var connection = this.connections[i];
|
||||
connection.node._setValueFromConnection(connection.inputPortName, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @name OutputProperty#hasConnections
|
||||
* @readonly
|
||||
*/
|
||||
hasConnections: {
|
||||
value: function() {
|
||||
return this.connections.length > 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
module.exports = OutputProperty;
|
||||
28
packages/noodl-runtime/src/projectsettings.js
Normal file
28
packages/noodl-runtime/src/projectsettings.js
Normal file
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
|
||||
function addModuleSettings(result, modules) {
|
||||
// Add module settings
|
||||
for(var i = 0; i < modules.length; i++) {
|
||||
var m = modules[i];
|
||||
if(m.settings) {
|
||||
m.settings.forEach(function(p) {
|
||||
result.ports.push(p);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateProjectSettings(projectSettings, modules) {
|
||||
const result = {
|
||||
dynamicports: [],
|
||||
ports: []
|
||||
};
|
||||
|
||||
addModuleSettings(result, modules);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateProjectSettings: generateProjectSettings
|
||||
};
|
||||
8
packages/noodl-runtime/src/services/services.js
Normal file
8
packages/noodl-runtime/src/services/services.js
Normal file
@@ -0,0 +1,8 @@
|
||||
var EventEmitter = require('../events');
|
||||
|
||||
function Services() {
|
||||
}
|
||||
|
||||
Services.events = new EventEmitter();
|
||||
|
||||
module.exports = Services;
|
||||
173
packages/noodl-runtime/src/timerscheduler.js
Normal file
173
packages/noodl-runtime/src/timerscheduler.js
Normal file
@@ -0,0 +1,173 @@
|
||||
'use strict';
|
||||
|
||||
function Timer(scheduler, args) {
|
||||
this.duration = args.duration || 0;
|
||||
this._isRunning = false;
|
||||
this._hasCalledOnStart = false;
|
||||
this.scheduler = scheduler;
|
||||
this.repeatCount = 1;
|
||||
this.delay = 0;
|
||||
|
||||
for(var arg in args) {
|
||||
this[arg] = args[arg];
|
||||
}
|
||||
}
|
||||
|
||||
Timer.prototype.start = function() {
|
||||
if(this._isRunning) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
this.scheduler.scheduleTimer(this);
|
||||
return this;
|
||||
};
|
||||
|
||||
Timer.prototype.stop = function() {
|
||||
this.scheduler.stopTimer(this);
|
||||
this._hasCalledOnStart = false;
|
||||
this._isRunning = false;
|
||||
this._wasStopped = true; // This is used to avoid calling onFinish
|
||||
};
|
||||
|
||||
Timer.prototype.isRunning = function() {
|
||||
return this._isRunning;
|
||||
};
|
||||
|
||||
Timer.prototype.durationLeft = function() {
|
||||
return this._durationLeft;
|
||||
};
|
||||
|
||||
function TimerScheduler(requestFrameCallback) {
|
||||
this.requestFrame = requestFrameCallback;
|
||||
this.runningTimers = [];
|
||||
this.newTimers = [];
|
||||
}
|
||||
|
||||
TimerScheduler.prototype.createTimer = function(args) {
|
||||
return new Timer(this, args);
|
||||
};
|
||||
|
||||
TimerScheduler.prototype.scheduleTimer = function(timer) {
|
||||
if(this.newTimers.indexOf(timer) === -1) {
|
||||
if(timer.repeatCount === 0) {
|
||||
timer.repeatCount = 100000;
|
||||
}
|
||||
|
||||
this.newTimers.push(timer);
|
||||
this.requestFrame();
|
||||
}
|
||||
};
|
||||
|
||||
TimerScheduler.prototype.stopTimer = function(timer) {
|
||||
var index;
|
||||
|
||||
if(timer._isRunning) {
|
||||
index = this.runningTimers.indexOf(timer);
|
||||
if(index !== -1) {
|
||||
this.runningTimers.splice(index, 1);
|
||||
}
|
||||
|
||||
if (timer.onStop && !timer._wasStopped) {
|
||||
timer.onStop();
|
||||
}
|
||||
}
|
||||
else {
|
||||
index = this.newTimers.indexOf(timer);
|
||||
if(index !== -1) {
|
||||
this.newTimers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TimerScheduler.prototype.runTimers = function(currentTime) {
|
||||
|
||||
var remainingTimers = [],
|
||||
finishedTimers = [],
|
||||
timersThisFrame = [];
|
||||
|
||||
var i,
|
||||
len = this.runningTimers.length,
|
||||
timer;
|
||||
|
||||
//copy timer list in case timers are added or removed during onStart or onRunning
|
||||
for(i=0; i<len; ++i) {
|
||||
timersThisFrame[i] = this.runningTimers[i];
|
||||
}
|
||||
|
||||
for(i=0; i<len; ++i) {
|
||||
timer = timersThisFrame[i];
|
||||
if(timer && currentTime >= timer._start) {
|
||||
|
||||
if(timer._hasCalledOnStart === false && timer.onStart) {
|
||||
timer.onStart();
|
||||
timer._hasCalledOnStart = true;
|
||||
}
|
||||
|
||||
var t;
|
||||
if(timer.duration > 0) {
|
||||
t = (currentTime - timer._start)/(timer.duration*timer.repeatCount);
|
||||
} else {
|
||||
t = 1.0;
|
||||
}
|
||||
|
||||
timer._durationLeft = timer.duration * (1-t);
|
||||
|
||||
var localT = t*timer.repeatCount - Math.floor(t*timer.repeatCount);
|
||||
if(t >= 1.0) {
|
||||
localT = 1.0;
|
||||
}
|
||||
|
||||
if(timer.onRunning) {
|
||||
timer.onRunning(localT);
|
||||
}
|
||||
|
||||
if(t < 1.0 && timer._isRunning) {
|
||||
remainingTimers.push(timer);
|
||||
}
|
||||
else if (!timer._wasStopped) {
|
||||
finishedTimers.push(timer);
|
||||
}
|
||||
} else {
|
||||
remainingTimers.push(timer);
|
||||
}
|
||||
}
|
||||
|
||||
this.runningTimers = remainingTimers;
|
||||
|
||||
for(i=0; i<finishedTimers.length; ++i) {
|
||||
finishedTimers[i]._isRunning = false;
|
||||
finishedTimers[i]._hasCalledOnStart = false;
|
||||
if( finishedTimers[i].onFinish ) {
|
||||
finishedTimers[i].onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
//add newly queued timers
|
||||
if(this.newTimers.length > 0) {
|
||||
for(i=0; i<this.newTimers.length; ++i) {
|
||||
timer = this.newTimers[i];
|
||||
timer._start = currentTime + timer.delay;
|
||||
timer._isRunning = true;
|
||||
timer._wasStopped = false;
|
||||
this.runningTimers.push(timer);
|
||||
|
||||
if(timer.delay === 0) {
|
||||
//play first timer frame directly to keep everything nicely synched
|
||||
if(timer.onStart) {
|
||||
timer.onStart();
|
||||
timer._hasCalledOnStart = true;
|
||||
}
|
||||
if(timer.onRunning) {
|
||||
timer.onRunning(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.newTimers.length = 0;
|
||||
}
|
||||
};
|
||||
|
||||
TimerScheduler.prototype.hasPendingTimers = function() {
|
||||
return this.runningTimers.length > 0 || this.newTimers.length > 0;
|
||||
};
|
||||
|
||||
module.exports = TimerScheduler;
|
||||
17
packages/noodl-runtime/src/utils.js
Normal file
17
packages/noodl-runtime/src/utils.js
Normal file
@@ -0,0 +1,17 @@
|
||||
//this just assumes the base url is '/' always
|
||||
function getAbsoluteUrl(_url) {
|
||||
|
||||
//convert to string in case the _url is a Cloud File (which is an object with a custom toString())
|
||||
const url = String(_url);
|
||||
|
||||
//only add a the base url if this is a local URL (e.g. not a https url or base64 string)
|
||||
if (!url || url[0] === "/" || url.includes("://") || url.startsWith('data:')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return (Noodl.baseUrl || '/') + url;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAbsoluteUrl
|
||||
};
|
||||
42
packages/noodl-runtime/src/variants.js
Normal file
42
packages/noodl-runtime/src/variants.js
Normal file
@@ -0,0 +1,42 @@
|
||||
class Variants {
|
||||
|
||||
constructor({graphModel, getNodeScope}) {
|
||||
this.getNodeScope = getNodeScope;
|
||||
|
||||
if(graphModel) {
|
||||
this.graphModel = graphModel;
|
||||
graphModel.on('variantUpdated', variant => this.onVariantUpdated(variant));
|
||||
}
|
||||
}
|
||||
|
||||
getVariant(typename, name) {
|
||||
if(!this.graphModel) return undefined;
|
||||
|
||||
return this.graphModel.getVariant(typename, name);
|
||||
}
|
||||
|
||||
onVariantUpdated(variant) {
|
||||
const nodeScope = this.getNodeScope();
|
||||
if(!nodeScope) return;
|
||||
|
||||
//get all nodes with the type the variant applies to
|
||||
const nodes = nodeScope.getNodesWithTypeRecursive(variant.typename);
|
||||
|
||||
//and filter for the ones using the updated variant
|
||||
const nodesWithVariant = nodes.filter(node => {
|
||||
|
||||
//if a variant has been set during runtime, it'll override the value from the model
|
||||
if(node.variant) return node.variant.name === variant.name;
|
||||
|
||||
//otherwise check the model (which always matches the editor)
|
||||
return node.model && node.model.variant === variant.name;
|
||||
});
|
||||
|
||||
//and re-apply the variant
|
||||
for(const node of nodesWithVariant) {
|
||||
node.setVariant(variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Variants;
|
||||
@@ -0,0 +1,55 @@
|
||||
const ActiveWarnings = require('./editorconnection.activewarnings');
|
||||
|
||||
describe('Tracks active warnings that are sent to the editor', ()=>{
|
||||
let activeWarnings;
|
||||
|
||||
beforeEach(async ()=>{
|
||||
activeWarnings = new ActiveWarnings();
|
||||
});
|
||||
|
||||
|
||||
test('Set and clear a warning', () => {
|
||||
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning')).toBe(true);
|
||||
|
||||
//these warnings shouldnt exist
|
||||
expect(activeWarnings.clearWarning('testId', 'otherKey')).toEqual(false);
|
||||
expect(activeWarnings.clearWarning('otherId', 'testKey')).toEqual(false);
|
||||
|
||||
//this warning is the one we set
|
||||
expect(activeWarnings.clearWarning('testId', 'testKey')).toEqual(true);
|
||||
|
||||
//and now the warning should be gone
|
||||
expect(activeWarnings.clearWarning('testId', 'testKey')).toEqual(false);
|
||||
});
|
||||
|
||||
test('Set and clear multiple warning on one node', () => {
|
||||
expect(activeWarnings.setWarning('testId', 'testKey1', 'testWarning')).toBe(true);
|
||||
expect(activeWarnings.setWarning('testId', 'testKey2', 'testWarning')).toBe(true);
|
||||
|
||||
expect(activeWarnings.clearWarning('testId', 'testKey1')).toEqual(true);
|
||||
expect(activeWarnings.clearWarning('testId', 'testKey1')).toEqual(false);
|
||||
|
||||
expect(activeWarnings.clearWarning('testId', 'testKey2')).toEqual(true);
|
||||
expect(activeWarnings.clearWarning('testId', 'testKey2')).toEqual(false);
|
||||
});
|
||||
|
||||
test('Clear multiple warnings at once', () => {
|
||||
expect(activeWarnings.setWarning('testId1', 'testKey1', 'testWarning')).toBe(true);
|
||||
expect(activeWarnings.setWarning('testId1', 'testKey2', 'testWarning')).toBe(true);
|
||||
expect(activeWarnings.setWarning('testId2', 'testKey1', 'testWarning')).toBe(true);
|
||||
|
||||
expect(activeWarnings.clearWarnings('testId3')).toEqual(false);
|
||||
|
||||
expect(activeWarnings.clearWarnings('testId1')).toEqual(true);
|
||||
expect(activeWarnings.clearWarnings('testId1')).toEqual(false);
|
||||
|
||||
expect(activeWarnings.clearWarnings('testId2')).toEqual(true);
|
||||
expect(activeWarnings.clearWarnings('testId2')).toEqual(false);
|
||||
});
|
||||
|
||||
test('Set same warning multiple times', () => {
|
||||
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning')).toBe(true);
|
||||
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning')).toBe(false);
|
||||
expect(activeWarnings.setWarning('testId', 'testKey', 'testWarning2')).toBe(true);
|
||||
});
|
||||
});
|
||||
98
packages/noodl-runtime/test/editormodeleventshandler.test.js
Normal file
98
packages/noodl-runtime/test/editormodeleventshandler.test.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const handleEvent = require('./editormodeleventshandler').handleEvent;
|
||||
const GraphModel = require('./models/graphmodel');
|
||||
const NodeContext = require('./nodecontext');
|
||||
|
||||
describe('Component ports update when on the componentPortsUpdated event', ()=>{
|
||||
let graphModel;
|
||||
let nodeContext;
|
||||
|
||||
beforeEach(async ()=>{
|
||||
nodeContext = new NodeContext();
|
||||
graphModel = new GraphModel();
|
||||
await graphModel.importComponentFromEditorData({
|
||||
id: 'component',
|
||||
name: 'testComponent',
|
||||
ports: [
|
||||
{ name: "testInput1", plug: "input", type: 'string'},
|
||||
{ name: "testInput2", plug: "input", type: 'string'},
|
||||
{ name: "testOutput1", plug: "output", type: 'string'},
|
||||
{ name: "testOutput2", plug: "output", type: 'string'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
function updateAndMatchPorts(newInputPorts, newOutputPorts) {
|
||||
|
||||
handleEvent(nodeContext, graphModel, {
|
||||
type: 'componentPortsUpdated',
|
||||
componentName: 'testComponent',
|
||||
ports: newInputPorts.concat(newOutputPorts)
|
||||
});
|
||||
|
||||
const componentModel = graphModel.getComponentWithName('testComponent');
|
||||
const inputPorts = componentModel.getInputPorts();
|
||||
for(const port of newInputPorts) {
|
||||
expect(inputPorts.hasOwnProperty(port.name)).toBeTruthy();
|
||||
expect(inputPorts[port.name].type).toEqual(port.type);
|
||||
}
|
||||
expect(Object.keys(inputPorts).length).toBe(newInputPorts.length);
|
||||
|
||||
const outputPorts = componentModel.getOutputPorts();
|
||||
for(const port of newOutputPorts) {
|
||||
expect(outputPorts.hasOwnProperty(port.name)).toBeTruthy();
|
||||
expect(outputPorts[port.name].type).toEqual(port.type);
|
||||
}
|
||||
expect(Object.keys(outputPorts).length).toBe(newOutputPorts.length);
|
||||
}
|
||||
|
||||
test('Input ports are added', () => {
|
||||
updateAndMatchPorts([
|
||||
{ name: "testInput1", plug: "input", type: 'string'},
|
||||
{ name: "testInput2", plug: "input", type: 'string'},
|
||||
],
|
||||
[
|
||||
{name: "testOutput1", plug: "output", type: 'string'}
|
||||
]);
|
||||
});
|
||||
|
||||
test('Input ports are removed', () => {
|
||||
updateAndMatchPorts([],
|
||||
[
|
||||
{name: "testOutput1", plug: "output", type: 'string'}
|
||||
]);
|
||||
});
|
||||
|
||||
test('Input port types are updated', () => {
|
||||
updateAndMatchPorts([
|
||||
{ name: "testInput1", plug: "input", type: 'boolean'},
|
||||
{ name: "testInput2", plug: "input", type: {name:'number'}}
|
||||
],
|
||||
[]);
|
||||
});
|
||||
|
||||
test('Output ports are added', () => {
|
||||
updateAndMatchPorts([
|
||||
{ name: "testInput1", plug: "input", type: 'string'}
|
||||
],
|
||||
[
|
||||
{name: "testOutput1", plug: "output", type: 'string'},
|
||||
{name: "testOutput2", plug: "output", type: 'string'}
|
||||
]);
|
||||
});
|
||||
|
||||
test('Output ports are removed', () => {
|
||||
updateAndMatchPorts([
|
||||
{ name: "testInput1", plug: "input", type: 'string'}
|
||||
],
|
||||
[
|
||||
]);
|
||||
});
|
||||
|
||||
test('Output port types are updated', () => {
|
||||
updateAndMatchPorts([],
|
||||
[
|
||||
{ name: "testOutput1", plug: "output", type: 'number'},
|
||||
{ name: "testOutput2", plug: "output", type: {name:'boolean'}}
|
||||
]);
|
||||
});
|
||||
});
|
||||
61
packages/noodl-runtime/test/eventsender.test.js
Normal file
61
packages/noodl-runtime/test/eventsender.test.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const EventSender = require('./eventsender');
|
||||
|
||||
describe('EventSender', ()=> {
|
||||
|
||||
let eventSender;
|
||||
|
||||
beforeEach(()=>{
|
||||
eventSender = new EventSender();
|
||||
});
|
||||
|
||||
it('can send events', ()=> {
|
||||
|
||||
const mockCallback = jest.fn(() => {});
|
||||
|
||||
eventSender.on('testEvent', mockCallback);
|
||||
|
||||
const someArgument = {lol:'troll'};
|
||||
eventSender.emit('testEvent', someArgument);
|
||||
|
||||
expect(mockCallback.mock.calls.length).toBe(1);
|
||||
expect(mockCallback.mock.calls[0][0]).toBe(someArgument);
|
||||
});
|
||||
|
||||
it('can remove a listener', ()=> {
|
||||
const mockCallback = jest.fn(() => {});
|
||||
const mockCallback2 = jest.fn(() => {});
|
||||
|
||||
const ref = {};
|
||||
const ref2 = {};
|
||||
|
||||
eventSender.on('testEvent', mockCallback, ref);
|
||||
eventSender.on('testEvent', mockCallback2, ref2);
|
||||
|
||||
eventSender.removeListenersWithRef(ref);
|
||||
eventSender.emit('testEvent');
|
||||
|
||||
expect(mockCallback.mock.calls.length).toBe(0);
|
||||
expect(mockCallback2.mock.calls.length).toBe(1);
|
||||
|
||||
});
|
||||
|
||||
it('can remove all listeners', ()=> {
|
||||
const mockCallback = jest.fn(() => {});
|
||||
const mockCallback2 = jest.fn(() => {});
|
||||
|
||||
const ref = {};
|
||||
const ref2 = {};
|
||||
|
||||
eventSender.on('testEvent', mockCallback, ref);
|
||||
eventSender.on('testEvent2', mockCallback2, ref2);
|
||||
|
||||
eventSender.removeAllListeners('testEvent');
|
||||
eventSender.emit('testEvent');
|
||||
eventSender.emit('testEvent2');
|
||||
|
||||
expect(mockCallback.mock.calls.length).toBe(0);
|
||||
expect(mockCallback2.mock.calls.length).toBe(1);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
67
packages/noodl-runtime/test/models/componentmodel.test.js
Normal file
67
packages/noodl-runtime/test/models/componentmodel.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const { ComponentModel } = require('./componentmodel');
|
||||
const NodeModel = require('./nodemodel');
|
||||
|
||||
test('Returns all nodes in the component', ()=>{
|
||||
const component = new ComponentModel('testComponent');
|
||||
|
||||
component.addNode(new NodeModel('id1', 'testNode'));
|
||||
let nodes = component.getAllNodes();
|
||||
expect(nodes.length).toBe(1);
|
||||
expect(nodes[0].id).toBe('id1');
|
||||
|
||||
component.addNode(new NodeModel('id2', 'testNode'));
|
||||
nodes = component.getAllNodes();
|
||||
expect(nodes.length).toBe(2);
|
||||
|
||||
//the order is not guaranteed, so let's just use find
|
||||
expect(nodes.find(n=>n.id==='id1')).not.toBe(undefined);
|
||||
expect(nodes.find(n=>n.id==='id2')).not.toBe(undefined);
|
||||
});
|
||||
|
||||
test('Connections can be added', ()=>{
|
||||
const component = new ComponentModel('testComponent');
|
||||
component.addNode(new NodeModel('id1', 'testNode'));
|
||||
component.addNode(new NodeModel('id2', 'testNode'));
|
||||
component.addConnection({
|
||||
sourceId: 'id1',
|
||||
sourcePort: 'test',
|
||||
targetId: 'id2',
|
||||
targetPort: 'test'
|
||||
});
|
||||
|
||||
const connections = component.getConnectionsFrom('id1');
|
||||
expect(connections.length).toBe(1);
|
||||
});
|
||||
|
||||
test('Connections can be removed', ()=>{
|
||||
const connection = {
|
||||
sourceId: 'id1',
|
||||
sourcePort: 'test',
|
||||
targetId: 'id2',
|
||||
targetPort: 'test'
|
||||
};
|
||||
|
||||
const component = new ComponentModel('testComponent');
|
||||
component.addNode(new NodeModel('id1', 'testNode'));
|
||||
component.addNode(new NodeModel('id2', 'testNode'));
|
||||
component.addConnection(connection);
|
||||
//test with a copy of the object, and not the same object, to verify it works then as well
|
||||
component.removeConnection(JSON.parse(JSON.stringify(connection)));
|
||||
expect(component.getAllConnections().length).toBe(0);
|
||||
});
|
||||
|
||||
test('Removing non-existing connection should not throw', ()=>{
|
||||
const connection = {
|
||||
sourceId: 'id1',
|
||||
sourcePort: 'test',
|
||||
targetId: 'id2',
|
||||
targetPort: 'test'
|
||||
};
|
||||
|
||||
const component = new ComponentModel('testComponent');
|
||||
component.addNode(new NodeModel('id1', 'testNode'));
|
||||
component.addNode(new NodeModel('id2', 'testNode'));
|
||||
component.addConnection(connection);
|
||||
expect(()=>component.removeConnection({})).not.toThrow();
|
||||
expect(component.getAllConnections().length).toBe(1);
|
||||
});
|
||||
50
packages/noodl-runtime/test/nodecontext.test.js
Normal file
50
packages/noodl-runtime/test/nodecontext.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const NodeContext = require('./nodecontext');
|
||||
const NodeDefinition = require('./nodedefinition');
|
||||
const ComponentInstance = require('./nodes/componentinstance');
|
||||
const { ComponentModel } = require('./models/componentmodel');
|
||||
|
||||
describe('NodeContext', ()=>{
|
||||
|
||||
test("can detect cyclic updates", async () => {
|
||||
const testNodeDefinition = NodeDefinition.defineNode({
|
||||
name: 'Test Node',
|
||||
category: 'test',
|
||||
initialize() {this._internal.count = 0;},
|
||||
inputs: {
|
||||
input: {
|
||||
set() {
|
||||
this._internal.count++;
|
||||
this.flagOutputDirty('output');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
output: {type: 'number', get() {return Math.random()}}
|
||||
},
|
||||
})
|
||||
|
||||
const context = new NodeContext();
|
||||
context.nodeRegister.register(testNodeDefinition);
|
||||
|
||||
const componentModel = await ComponentModel.createFromExportData({
|
||||
name: 'testComponent',
|
||||
id: '1',
|
||||
nodes: [
|
||||
{ id: '2', type: 'Test Node', parameters: {input: true}},
|
||||
{ id: '3', type: 'Test Node'}
|
||||
],
|
||||
connections: [
|
||||
{sourceId: '2', sourcePort: 'output', targetId: '3', targetPort: 'input'},
|
||||
{sourceId: '3', sourcePort: 'output', targetId: '2', targetPort: 'input'}
|
||||
]
|
||||
});
|
||||
|
||||
const componentInstance = new ComponentInstance(context);
|
||||
await componentInstance.setComponentModel(componentModel);
|
||||
|
||||
context.update();
|
||||
|
||||
expect(componentInstance.nodeScope.getNodeWithId('2')._internal.count).toBeGreaterThan(50);
|
||||
expect(componentInstance.nodeScope.getNodeWithId('3')._internal.count).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
241
packages/noodl-runtime/test/nodes/componentinstance.test.js
Normal file
241
packages/noodl-runtime/test/nodes/componentinstance.test.js
Normal file
@@ -0,0 +1,241 @@
|
||||
const ComponentInstance = require('./componentinstance');
|
||||
const { ComponentModel } = require('../models/componentmodel');
|
||||
const GraphModel = require('../models/graphmodel');
|
||||
const NodeContext = require('../nodecontext');
|
||||
const ComponentInputs = require('./componentinputs');
|
||||
const ComponentOutputs = require('./componentoutputs');
|
||||
const NodeDefinition = require('../nodedefinition');
|
||||
|
||||
async function setupComponent() {
|
||||
const context = new NodeContext();
|
||||
context.nodeRegister.register(NodeDefinition.defineNode(ComponentInputs.node));
|
||||
context.nodeRegister.register(NodeDefinition.defineNode(ComponentOutputs.node));
|
||||
|
||||
const componentModel = await ComponentModel.createFromExportData({
|
||||
name: 'testComponent',
|
||||
id: 'loltroll2',
|
||||
nodes: [
|
||||
{
|
||||
id: 'loltroll',
|
||||
type: 'Component Inputs',
|
||||
ports: [{ name: 'textInput', plug: 'output', type: 'string' }]
|
||||
},
|
||||
{
|
||||
id: 'componentOutputs',
|
||||
type: 'Component Outputs',
|
||||
ports: [{ name: 'textOutput', plug: 'input', type: 'string' }]
|
||||
}
|
||||
],
|
||||
ports: [
|
||||
{ name: 'textInput', plug: 'input', type: 'string' },
|
||||
{ name: 'textOutput', plug: 'output', type: 'string' }
|
||||
],
|
||||
connections: []
|
||||
});
|
||||
|
||||
const componentInstance = new ComponentInstance(context);
|
||||
await componentInstance.setComponentModel(componentModel);
|
||||
return { componentInstance, componentModel };
|
||||
}
|
||||
|
||||
function createTestNodeDefinition() {
|
||||
return NodeDefinition.defineNode({
|
||||
name: 'Test Node',
|
||||
category: 'test',
|
||||
initialize() {
|
||||
this.inputHistory = [];
|
||||
},
|
||||
inputs: {
|
||||
input: {
|
||||
type: 'string',
|
||||
set(value) {
|
||||
this.testValue = value;
|
||||
this.flagOutputDirty('output');
|
||||
this.inputHistory.push(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
output: {
|
||||
type: 'string',
|
||||
get() {
|
||||
return this.testValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('Component inputs are registered', async () => {
|
||||
const { componentInstance } = await setupComponent();
|
||||
expect(componentInstance.hasInput('textInput')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Internal component inputs are removed when a component input node is removed', async () => {
|
||||
const { componentInstance, componentModel } = await setupComponent();
|
||||
|
||||
expect(componentInstance._internal.componentInputs.length).toBe(1);
|
||||
await componentModel.removeNodeWithId('loltroll');
|
||||
expect(componentInstance._internal.componentInputs.length).toBe(0);
|
||||
});
|
||||
|
||||
test('Component outputs are registered', async () => {
|
||||
const { componentInstance } = await setupComponent();
|
||||
expect(componentInstance.hasOutput('textOutput')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Internal component outputs are removed when a component output node is removed', async () => {
|
||||
const { componentInstance, componentModel } = await setupComponent();
|
||||
|
||||
expect(componentInstance._internal.componentOutputs.length).toBe(1);
|
||||
await componentModel.removeNodeWithId('componentOutputs');
|
||||
expect(componentInstance._internal.componentOutputs.length).toBe(0);
|
||||
});
|
||||
|
||||
test('Parameters should be overwritten by connections', async () => {
|
||||
const context = new NodeContext();
|
||||
|
||||
context.nodeRegister.register(createTestNodeDefinition());
|
||||
|
||||
const componentModel = await ComponentModel.createFromExportData({
|
||||
name: 'rootComponent',
|
||||
id: 'testid',
|
||||
nodes: [
|
||||
{
|
||||
id: 'testNodeEnd',
|
||||
type: 'Test Node',
|
||||
parameters: { input: 'param-value' }
|
||||
},
|
||||
{
|
||||
id: 'testNodeStart',
|
||||
type: 'Test Node',
|
||||
parameters: { input: 'connection-value' }
|
||||
}
|
||||
],
|
||||
connections: [{ sourceId: 'testNodeStart', sourcePort: 'output', targetId: 'testNodeEnd', targetPort: 'input' }]
|
||||
});
|
||||
|
||||
const componentInstance = new ComponentInstance(context);
|
||||
await componentInstance.setComponentModel(componentModel);
|
||||
|
||||
const testnodeEnd = componentInstance.nodeScope.getNodeWithId('testNodeEnd');
|
||||
testnodeEnd.update();
|
||||
|
||||
expect(testnodeEnd.inputHistory.length).toBe(1);
|
||||
expect(testnodeEnd.inputHistory[0]).toBe('connection-value');
|
||||
});
|
||||
|
||||
test('Component inputs should not interfere with internal nodes', async () => {
|
||||
const context = new NodeContext();
|
||||
|
||||
context.nodeRegister.register(NodeDefinition.defineNode(ComponentInputs.node));
|
||||
context.nodeRegister.register(createTestNodeDefinition());
|
||||
|
||||
const componentModel = await ComponentModel.createFromExportData({
|
||||
name: 'rootComponent',
|
||||
id: 'testid',
|
||||
nodes: [
|
||||
{
|
||||
id: 'testnode',
|
||||
type: 'Test Node',
|
||||
parameters: { input: 'param-value' }
|
||||
},
|
||||
{
|
||||
id: 'compinput',
|
||||
type: 'Component Inputs',
|
||||
ports: [{ name: 'output', plug: 'output', type: 'string' }]
|
||||
}
|
||||
],
|
||||
connections: [{ sourceId: 'compinput', sourcePort: 'output', targetId: 'testnode', targetPort: 'input' }]
|
||||
});
|
||||
|
||||
const componentInstance = new ComponentInstance(context);
|
||||
await componentInstance.setComponentModel(componentModel);
|
||||
|
||||
const testnode = componentInstance.nodeScope.getNodeWithId('testnode');
|
||||
testnode.update();
|
||||
|
||||
expect(testnode.inputHistory.length).toBe(1);
|
||||
expect(testnode.inputHistory[0]).toBe('param-value');
|
||||
});
|
||||
|
||||
test('No delays in component inputs and outputs', async () => {
|
||||
const context = new NodeContext();
|
||||
|
||||
context.nodeRegister.register(NodeDefinition.defineNode(ComponentInputs.node));
|
||||
context.nodeRegister.register(NodeDefinition.defineNode(ComponentOutputs.node));
|
||||
context.nodeRegister.register(createTestNodeDefinition());
|
||||
|
||||
const graph = new GraphModel();
|
||||
|
||||
graph.on('componentAdded', (component) => context.registerComponentModel(component));
|
||||
|
||||
await graph.importEditorData({
|
||||
components: [
|
||||
{
|
||||
name: 'testComponent',
|
||||
nodes: [
|
||||
{
|
||||
id: 'compinput',
|
||||
type: 'Component Inputs',
|
||||
ports: [{ name: 'textInput', plug: 'output', type: 'string' }]
|
||||
},
|
||||
{
|
||||
id: 'testnode-inner',
|
||||
type: 'Test Node',
|
||||
parameters: { input: 'inner-value' }
|
||||
},
|
||||
{
|
||||
id: 'compoutput',
|
||||
type: 'Component Outputs',
|
||||
ports: [{ name: 'textOutput', plug: 'input', type: 'string' }]
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
{ sourceId: 'compinput', sourcePort: 'textInput', targetId: 'testnode-inner', targetPort: 'input' },
|
||||
{ sourceId: 'testnode-inner', sourcePort: 'output', targetId: 'compoutput', targetPort: 'textOutput' }
|
||||
],
|
||||
ports: [
|
||||
{ name: 'textInput', plug: 'input', type: 'string' },
|
||||
{ name: 'textOutput', plug: 'output', type: 'string' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'rootComponent',
|
||||
nodes: [
|
||||
{
|
||||
id: 'testComponent',
|
||||
type: 'testComponent'
|
||||
},
|
||||
{
|
||||
id: 'testnodeEnd',
|
||||
type: 'Test Node'
|
||||
},
|
||||
{
|
||||
id: 'testNodeStart',
|
||||
type: 'Test Node',
|
||||
parameters: { input: 'outer-value' }
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
{ sourceId: 'testComponent', sourcePort: 'textOutput', targetId: 'testnodeEnd', targetPort: 'input' },
|
||||
{ sourceId: 'testNodeStart', sourcePort: 'output', targetId: 'testComponent', targetPort: 'textInput' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const rootComponent = await context.createComponentInstanceNode('rootComponent');
|
||||
context.setRootComponent(rootComponent);
|
||||
|
||||
const testnodeEnd = rootComponent.nodeScope.getNodeWithId('testnodeEnd');
|
||||
const testnodeInner = rootComponent.nodeScope.getNodesWithIdRecursive('testnode-inner')[0];
|
||||
|
||||
context.update();
|
||||
|
||||
expect(testnodeEnd.inputHistory.length).toBe(1);
|
||||
expect(testnodeEnd.inputHistory[0]).toBe('outer-value');
|
||||
|
||||
expect(testnodeInner.inputHistory.length).toBe(1);
|
||||
expect(testnodeInner.inputHistory[0]).toBe('outer-value');
|
||||
});
|
||||
157
packages/noodl-runtime/test/nodescope.test.js
Normal file
157
packages/noodl-runtime/test/nodescope.test.js
Normal file
@@ -0,0 +1,157 @@
|
||||
const NodeScope = require('./nodescope');
|
||||
const NodeContext = require('./nodecontext');
|
||||
const NodeDefinition = require('./nodedefinition');
|
||||
const { ComponentModel } = require('./models/componentmodel');
|
||||
const GraphModel = require('./models/graphmodel');
|
||||
|
||||
function createTestNodeDefinition({onInit, onDelete}) {
|
||||
return NodeDefinition.defineNode({
|
||||
name: 'Test Node',
|
||||
category: 'test',
|
||||
initialize() {
|
||||
this.children = [];
|
||||
onInit && onInit.call(this);
|
||||
onDelete && this.addDeleteListener(onDelete);
|
||||
},
|
||||
methods: {
|
||||
addChild(child) {
|
||||
this.children.push(child);
|
||||
},
|
||||
removeChild(child) {
|
||||
this.children.splice(this.children.indexOf(child), 1);
|
||||
},
|
||||
getChildren() {
|
||||
return this.children;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Create a graph that includes a two levels of component children
|
||||
async function createTestRootComponent(args) {
|
||||
const context = new NodeContext();
|
||||
|
||||
context.nodeRegister.register(createTestNodeDefinition(args || {}));
|
||||
|
||||
const graph = new GraphModel();
|
||||
|
||||
graph.on("componentAdded", component => context.registerComponentModel(component) );
|
||||
|
||||
await graph.importEditorData( {
|
||||
components: [
|
||||
{
|
||||
name: 'rootComponent',
|
||||
nodes: [ {
|
||||
id: 'test-component',
|
||||
type: 'testComponent',
|
||||
children: [
|
||||
{ id: 'test-node-from-root', type: 'Test Node' },
|
||||
{
|
||||
id: 'test-component-child',
|
||||
type: 'testComponent',
|
||||
children: [
|
||||
{ id: 'test-node-in-component-child', type: 'Test Node' },
|
||||
]
|
||||
}
|
||||
]
|
||||
} ],
|
||||
},
|
||||
{
|
||||
name: 'testComponent',
|
||||
nodes: [ {
|
||||
id: 'test-node',
|
||||
type: 'Test Node',
|
||||
children: [
|
||||
{
|
||||
id: 'test-node-child',
|
||||
type: 'Test Node',
|
||||
children: [{id: 'component-children', type: 'Component Children'}]
|
||||
}
|
||||
]
|
||||
} ],
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const rootComponent = await context.createComponentInstanceNode("rootComponent");
|
||||
context.setRootComponent(rootComponent);
|
||||
|
||||
return rootComponent;
|
||||
}
|
||||
|
||||
test('find nodes by id', async () => {
|
||||
const nodeScope = (await createTestRootComponent()).nodeScope;
|
||||
|
||||
expect(nodeScope.hasNodeWithId('test-component')).toBe(true);
|
||||
|
||||
const testComponent = nodeScope.getNodeWithId('test-component');
|
||||
expect(testComponent.nodeScope.hasNodeWithId('test-node')).toBe(true);
|
||||
});
|
||||
|
||||
test('delete component', async () => {
|
||||
const nodeScope = (await createTestRootComponent()).nodeScope;
|
||||
|
||||
nodeScope.deleteNode(nodeScope.getNodeWithId('test-component'));
|
||||
expect(nodeScope.hasNodeWithId('test-node')).toBe(false);
|
||||
});
|
||||
|
||||
test('delete hierarchy', async () => {
|
||||
let testNodeCount = 0;
|
||||
const testComponent = await createTestRootComponent({
|
||||
onInit: () => testNodeCount++,
|
||||
onDelete: () => testNodeCount--
|
||||
});
|
||||
|
||||
const nodeScope = testComponent.nodeScope.getNodeWithId('test-component').nodeScope;
|
||||
|
||||
expect(testNodeCount).toBe(6);
|
||||
nodeScope.deleteNode(nodeScope.getNodeWithId('test-node'));
|
||||
expect(nodeScope.hasNodeWithId('test-node')).toBe(false);
|
||||
expect(nodeScope.hasNodeWithId('test-node-child')).toBe(false);
|
||||
expect(testNodeCount).toBe(3);
|
||||
});
|
||||
|
||||
test('delete child in a component children hierarchy', async () => {
|
||||
let testNodeCount = 0;
|
||||
const testComponent = await createTestRootComponent({
|
||||
onInit: () => testNodeCount++,
|
||||
onDelete: () => testNodeCount--
|
||||
});
|
||||
const nodeScope = testComponent.nodeScope;
|
||||
|
||||
expect(testNodeCount).toBe(6);
|
||||
expect(nodeScope.hasNodeWithId('test-node-from-root')).toBe(true);
|
||||
nodeScope.deleteNode(nodeScope.getNodeWithId('test-node-from-root'));
|
||||
expect(nodeScope.hasNodeWithId('test-node-from-root')).toBe(false);
|
||||
|
||||
expect(testNodeCount).toBe(5);
|
||||
});
|
||||
|
||||
test('delete component with component children', async () => {
|
||||
let testNodeCount = 0;
|
||||
const testComponent = await createTestRootComponent({
|
||||
onInit: () => testNodeCount++,
|
||||
onDelete: () => testNodeCount--
|
||||
});
|
||||
const nodeScope = testComponent.nodeScope;
|
||||
|
||||
expect(testNodeCount).toBe(6);
|
||||
const node = nodeScope.getNodeWithId('test-component-child');
|
||||
nodeScope.deleteNode(node);
|
||||
expect(testNodeCount).toBe(3);
|
||||
});
|
||||
|
||||
test('delete entire scope and nested scopes', async () => {
|
||||
|
||||
let testNodeCount = 0;
|
||||
|
||||
const testComponent = await createTestRootComponent({
|
||||
onInit: () => testNodeCount++,
|
||||
onDelete: () => testNodeCount--
|
||||
});
|
||||
const nodeScope = testComponent.nodeScope;
|
||||
|
||||
expect(testNodeCount).toBe(6);
|
||||
nodeScope.reset();
|
||||
expect(testNodeCount).toBe(0);
|
||||
});
|
||||
7
packages/noodl-runtime/test/outputproperty.test.js
Normal file
7
packages/noodl-runtime/test/outputproperty.test.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const OutputProperty = require('./outputproperty');
|
||||
|
||||
test('Throws an exception if no owner is specified', () => {
|
||||
expect(()=>{
|
||||
new OutputProperty();
|
||||
}).toThrow(Error);
|
||||
});
|
||||
Reference in New Issue
Block a user