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:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View 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.

View 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;

View 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"
}
}

View 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;

View 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;

View 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;

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

View 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;

View 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;

View 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;

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

View File

@@ -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;

View 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;

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

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

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

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

View 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;

View 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;

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

View 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;

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

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -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;

View File

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

View File

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

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

View File

@@ -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;

View File

@@ -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;

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

View File

@@ -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;

View File

@@ -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;

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

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

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

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

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

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

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

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View 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;

View 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;

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

View File

@@ -0,0 +1,8 @@
var EventEmitter = require('../events');
function Services() {
}
Services.events = new EventEmitter();
module.exports = Services;

View 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;

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

View 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;

View File

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

View 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'}}
]);
});
});

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

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

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

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

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

View File

@@ -0,0 +1,7 @@
const OutputProperty = require('./outputproperty');
test('Throws an exception if no owner is specified', () => {
expect(()=>{
new OutputProperty();
}).toThrow(Error);
});