mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-11 23:02:55 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
54
packages/noodl-runtime/src/nodes/std-library/and.js
Normal file
54
packages/noodl-runtime/src/nodes/std-library/and.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const AndNode = {
|
||||
name: 'And',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/and',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return and(this._internal.inputs);
|
||||
},
|
||||
numberedInputs: {
|
||||
input: {
|
||||
displayPrefix: 'Input',
|
||||
type: 'boolean',
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
value = value ? true : false;
|
||||
|
||||
if (this._internal.inputs[index] === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.inputs[index] = value;
|
||||
const result = and(this._internal.inputs);
|
||||
|
||||
if (this._internal.result !== result) {
|
||||
this._internal.result = result;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Result',
|
||||
get() {
|
||||
return this._internal.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: AndNode
|
||||
};
|
||||
|
||||
function and(values) {
|
||||
//if none are false, then return true
|
||||
return values.length > 0 && values.some((v) => !v) === false;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const BooleanToStringNode = {
|
||||
name: 'Boolean To String',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/boolean-to-string',
|
||||
category: 'Utilities',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
this._internal.currentSelectedIndex = 0;
|
||||
this._internal.indexChanged = false;
|
||||
|
||||
this._internal.trueString = '';
|
||||
this._internal.falseString = '';
|
||||
},
|
||||
inputs: {
|
||||
trueString: {
|
||||
displayName: 'String for true',
|
||||
type: 'string',
|
||||
set: function (value) {
|
||||
if (this._internal.trueString === value) return;
|
||||
this._internal.trueString = value;
|
||||
|
||||
if (this._internal.currentInput) {
|
||||
this.flagOutputDirty('currentValue');
|
||||
}
|
||||
}
|
||||
},
|
||||
falseString: {
|
||||
displayName: 'String for false',
|
||||
type: 'string',
|
||||
set: function (value) {
|
||||
if (this._internal.falseString === value) return;
|
||||
this._internal.falseString = value;
|
||||
|
||||
if (!this._internal.currentInput) {
|
||||
this.flagOutputDirty('currentValue');
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
type: { name: 'boolean' },
|
||||
displayName: 'Selector',
|
||||
set: function (value) {
|
||||
if (this._internal.currentInput === value) return;
|
||||
|
||||
this._internal.currentInput = value;
|
||||
this.flagOutputDirty('currentValue');
|
||||
this.sendSignalOnOutput('inputChanged');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentValue: {
|
||||
type: 'string',
|
||||
displayName: 'Current Value',
|
||||
group: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.currentInput ? this._internal.trueString : this._internal.falseString;
|
||||
}
|
||||
},
|
||||
inputChanged: {
|
||||
type: 'signal',
|
||||
displayName: 'Selector Changed',
|
||||
group: 'Signals'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: BooleanToStringNode
|
||||
};
|
||||
86
packages/noodl-runtime/src/nodes/std-library/condition.js
Normal file
86
packages/noodl-runtime/src/nodes/std-library/condition.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
const ConditionNode = {
|
||||
name: 'Condition',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/logic/condition',
|
||||
category: 'Logic',
|
||||
initialize: function () {},
|
||||
getInspectInfo() {
|
||||
const condition = this.getInputValue('condition');
|
||||
let value;
|
||||
if (condition === undefined) {
|
||||
value = '[No input]';
|
||||
}
|
||||
value = condition;
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
displayName: 'Condition',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
if (!this.isInputConnected('eval')) {
|
||||
// Evaluate right away
|
||||
this.scheduleEvaluate();
|
||||
}
|
||||
}
|
||||
},
|
||||
eval: {
|
||||
type: 'signal',
|
||||
displayName: 'Evaluate',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleEvaluate();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
ontrue: {
|
||||
type: 'signal',
|
||||
displayName: 'On True',
|
||||
group: 'Events'
|
||||
},
|
||||
onfalse: {
|
||||
type: 'signal',
|
||||
displayName: 'On False',
|
||||
group: 'Events'
|
||||
},
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Is True',
|
||||
group: 'Booleans',
|
||||
get() {
|
||||
return !!this.getInputValue('condition');
|
||||
}
|
||||
},
|
||||
isfalse: {
|
||||
type: 'boolean',
|
||||
displayName: 'Is False',
|
||||
group: 'Booleans',
|
||||
get() {
|
||||
return !this.getInputValue('condition');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleEvaluate() {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.flagOutputDirty('result');
|
||||
this.flagOutputDirty('isfalse');
|
||||
|
||||
const condition = this.getInputValue('condition');
|
||||
this.sendSignalOnOutput(condition ? 'ontrue' : 'onfalse');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ConditionNode
|
||||
};
|
||||
124
packages/noodl-runtime/src/nodes/std-library/counter.js
Normal file
124
packages/noodl-runtime/src/nodes/std-library/counter.js
Normal file
@@ -0,0 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
const CounterNode = {
|
||||
name: 'Counter',
|
||||
docs: 'https://docs.noodl.net/nodes/math/counter',
|
||||
category: 'Math',
|
||||
initialize: function () {
|
||||
this._internal.currentValue = 0;
|
||||
this._internal.startValue = 0;
|
||||
this._internal.startValueSet = false;
|
||||
|
||||
this._internal.limitsEnabled = false;
|
||||
this._internal.limitsMin = 0;
|
||||
this._internal.limitsMax = 0;
|
||||
},
|
||||
getInspectInfo() {
|
||||
return 'Count: ' + this._internal.currentValue;
|
||||
},
|
||||
inputs: {
|
||||
increase: {
|
||||
group: 'Actions',
|
||||
displayName: 'Increase Count',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.limitsEnabled && this._internal.currentValue >= this._internal.limitsMax) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.currentValue++;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
},
|
||||
decrease: {
|
||||
group: 'Actions',
|
||||
displayName: 'Decrease Count',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.limitsEnabled && this._internal.currentValue <= this._internal.limitsMin) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.currentValue--;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
group: 'Actions',
|
||||
displayName: 'Reset To Start',
|
||||
valueChangedToTrue: function () {
|
||||
if (this.currentValue === 0) {
|
||||
return;
|
||||
}
|
||||
this._internal.currentValue = this._internal.startValue;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
},
|
||||
startValue: {
|
||||
type: 'number',
|
||||
displayName: 'Start Value',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.startValue = Number(value);
|
||||
|
||||
if (this._internal.startValueSet === false) {
|
||||
this._internal.startValueSet = true;
|
||||
this._internal.currentValue = this._internal.startValue;
|
||||
this.flagOutputDirty('currentCount');
|
||||
this.sendSignalOnOutput('countChanged');
|
||||
}
|
||||
}
|
||||
},
|
||||
limitsMin: {
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
displayName: 'Min Value',
|
||||
group: 'Limits',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.limitsMin = Number(value);
|
||||
}
|
||||
},
|
||||
limitsMax: {
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
displayName: 'Max Value',
|
||||
group: 'Limits',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.limitsMax = Number(value);
|
||||
}
|
||||
},
|
||||
limitsEnabled: {
|
||||
type: {
|
||||
name: 'boolean'
|
||||
},
|
||||
displayName: 'Limits Enabled',
|
||||
group: 'Limits',
|
||||
default: false,
|
||||
set: function (value) {
|
||||
this._internal.limitsEnabled = value ? true : false;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentCount: {
|
||||
displayName: 'Current Count',
|
||||
type: 'number',
|
||||
getter: function () {
|
||||
return this._internal.currentValue;
|
||||
}
|
||||
},
|
||||
countChanged: {
|
||||
displayName: 'Count Changed',
|
||||
type: 'signal'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CounterNode
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
const CloudFile = require('../../../api/cloudfile');
|
||||
|
||||
const CloudFileNode = {
|
||||
name: 'Cloud File',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/cloud-file',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
getInspectInfo() {
|
||||
return this._internal.cloudFile && this._internal.cloudFile.getUrl();
|
||||
},
|
||||
outputs: {
|
||||
url: {
|
||||
type: 'string',
|
||||
displayName: 'URL',
|
||||
group: 'General',
|
||||
get() {
|
||||
return this._internal.cloudFile && this._internal.cloudFile.getUrl();
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
displayName: 'Name',
|
||||
group: 'General',
|
||||
get() {
|
||||
if (!this._internal.cloudFile) return;
|
||||
|
||||
//parse prefixes the file with a guid_
|
||||
//remove it so the name is the same as the original file name
|
||||
const n = this._internal.cloudFile.getName().split('_');
|
||||
return n.length === 1 ? n[0] : n.slice(1).join('_');
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
file: {
|
||||
type: 'cloudfile',
|
||||
displayName: 'Cloud File',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
if (value instanceof CloudFile === false) {
|
||||
return;
|
||||
}
|
||||
this._internal.cloudFile = value;
|
||||
this.flagOutputDirty('name');
|
||||
this.flagOutputDirty('url');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CloudFileNode
|
||||
};
|
||||
@@ -0,0 +1,722 @@
|
||||
const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');
|
||||
|
||||
const Model = require('../../../model'),
|
||||
Collection = require('../../../collection'),
|
||||
CloudStore = require('../../../api/cloudstore'),
|
||||
JavascriptNodeParser = require('../../../javascriptnodeparser'),
|
||||
QueryUtils = require('../../../api/queryutils');
|
||||
|
||||
var DbCollectionNode = {
|
||||
name: 'DbCollection2',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/query-records',
|
||||
displayName: 'Query Records',
|
||||
/* shortDesc: "A database collection.",*/
|
||||
category: 'Cloud Services',
|
||||
usePortAsLabel: 'collectionName',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
this._internal.queryParameters = {};
|
||||
|
||||
var collectionChangedScheduled = false;
|
||||
this._internal.collectionChangedCallback = function () {
|
||||
//this can be called multiple times when adding/removing more than one item
|
||||
//so optimize by only updating outputs once
|
||||
if (collectionChangedScheduled) return;
|
||||
collectionChangedScheduled = true;
|
||||
|
||||
_this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
collectionChangedScheduled = false;
|
||||
});
|
||||
};
|
||||
|
||||
this._internal.cloudStoreEvents = function (args) {
|
||||
if (_this.isInputConnected('storageFetch') === true) return;
|
||||
|
||||
if (_this._internal.collection === undefined) return;
|
||||
if (args.collection !== _this._internal.name) return;
|
||||
|
||||
function _addModelAtCorrectIndex(m) {
|
||||
if (_this._internal.currentQuery.sort !== undefined) {
|
||||
// We need to add it at the right index
|
||||
for (var i = 0; i < _this._internal.collection.size(); i++)
|
||||
if (QueryUtils.compareObjects(_this._internal.currentQuery.sort, _this._internal.collection.get(i), m) > 0)
|
||||
break;
|
||||
|
||||
_this._internal.collection.addAtIndex(m, i);
|
||||
} else {
|
||||
_this._internal.collection.add(m);
|
||||
}
|
||||
|
||||
// Make sure we don't exceed limit
|
||||
let size = _this._internal.collection.size();
|
||||
if (_this._internal.currentQuery.limit !== undefined && size > _this._internal.currentQuery.limit)
|
||||
_this._internal.collection.remove(
|
||||
_this._internal.collection.get(
|
||||
_this._internal.currentQuery.sort !== undefined && _this._internal.currentQuery.sort[0][0] === '-'
|
||||
? size - 1
|
||||
: 0
|
||||
)
|
||||
);
|
||||
|
||||
//Send the array again over the items output to trigger function nodes etc that might be connected
|
||||
_this.flagOutputDirty('items');
|
||||
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
}
|
||||
|
||||
if (args.type === 'create') {
|
||||
const m = Model.get(args.object.objectId);
|
||||
if (m !== undefined) {
|
||||
// Check if the object matches the current query
|
||||
if (QueryUtils.matchesQuery(m, _this._internal.currentQuery.where)) {
|
||||
// If matches the query, add the item to results
|
||||
_addModelAtCorrectIndex(m);
|
||||
}
|
||||
}
|
||||
} else if (args.type === 'save') {
|
||||
const m = Model.get(args.objectId);
|
||||
if (m !== undefined) {
|
||||
const matchesQuery = QueryUtils.matchesQuery(m, _this._internal.currentQuery.where);
|
||||
|
||||
if (!matchesQuery && _this._internal.collection.contains(m)) {
|
||||
// The record no longer matches the filter, remove it
|
||||
_this._internal.collection.remove(m);
|
||||
|
||||
//Send the array again over the items output to trigger function nodes etc that might be connected
|
||||
_this.flagOutputDirty('items');
|
||||
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
} else if (matchesQuery && !_this._internal.collection.contains(m)) {
|
||||
// It's not part of the result collection but now matches they query, add it and resort
|
||||
_addModelAtCorrectIndex(m);
|
||||
}
|
||||
}
|
||||
} else if (args.type === 'delete') {
|
||||
const m = Model.get(args.objectId);
|
||||
if (m !== undefined) {
|
||||
_this._internal.collection.remove(m);
|
||||
|
||||
//Send the array again over the items output to trigger function nodes etc that might be connected
|
||||
_this.flagOutputDirty('items');
|
||||
|
||||
_this.flagOutputDirty('count');
|
||||
_this.flagOutputDirty('firstItemId');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listening to cloud store events is only for the global model scope, only valid in browser
|
||||
// in cloud runtime its a nop
|
||||
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
|
||||
cloudstore.on('save', this._internal.cloudStoreEvents);
|
||||
cloudstore.on('create', this._internal.cloudStoreEvents);
|
||||
cloudstore.on('delete', this._internal.cloudStoreEvents);
|
||||
|
||||
this._internal.storageSettings = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
const collection = this._internal.collection;
|
||||
if (!collection) {
|
||||
return { type: 'text', value: '[Not executed yet]' };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value: collection.items
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.collection;
|
||||
}
|
||||
},
|
||||
firstItemId: {
|
||||
type: 'string',
|
||||
displayName: 'First Record Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if (this._internal.collection) {
|
||||
var firstItem = this._internal.collection.get(0);
|
||||
if (firstItem !== undefined) return firstItem.getId();
|
||||
}
|
||||
}
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
displayName: 'Count',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.collection ? this._internal.collection.size() : 0;
|
||||
}
|
||||
},
|
||||
fetched: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Success'
|
||||
},
|
||||
failure: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Failure'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setCollectionName: function (name) {
|
||||
this._internal.name = name;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
setCollection: function (collection) {
|
||||
this.bindCollection(collection);
|
||||
this.flagOutputDirty('firstItemId');
|
||||
this.flagOutputDirty('items');
|
||||
this.flagOutputDirty('count');
|
||||
},
|
||||
unbindCurrentCollection: function () {
|
||||
var collection = this._internal.collection;
|
||||
if (!collection) return;
|
||||
collection.off('change', this._internal.collectionChangedCallback);
|
||||
this._internal.collection = undefined;
|
||||
},
|
||||
bindCollection: function (collection) {
|
||||
this.unbindCurrentCollection();
|
||||
this._internal.collection = collection;
|
||||
collection && collection.on('change', this._internal.collectionChangedCallback);
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this.unbindCurrentCollection();
|
||||
|
||||
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
|
||||
cloudstore.off('insert', this._internal.cloudStoreEvents);
|
||||
cloudstore.off('delete', this._internal.cloudStoreEvents);
|
||||
cloudstore.off('save', this._internal.cloudStoreEvents);
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.err = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
if (internal.fetchScheduled) return;
|
||||
internal.fetchScheduled = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
internal.fetchScheduled = false;
|
||||
|
||||
this.fetch();
|
||||
});
|
||||
},
|
||||
fetch: function () {
|
||||
if (this.context.editorConnection) {
|
||||
if (this._internal.name === undefined) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection', {
|
||||
message: 'No collection specified for query'
|
||||
});
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection');
|
||||
}
|
||||
}
|
||||
|
||||
const _c = Collection.get();
|
||||
const f = this.getStorageFilter();
|
||||
const limit = this.getStorageLimit();
|
||||
const skip = this.getStorageSkip();
|
||||
const count = this.getStorageFetchTotalCount();
|
||||
this._internal.currentQuery = {
|
||||
where: f.where,
|
||||
sort: f.sort,
|
||||
limit: limit,
|
||||
skip: skip
|
||||
};
|
||||
CloudStore.forScope(this.nodeScope.modelScope).query({
|
||||
collection: this._internal.name,
|
||||
where: f.where,
|
||||
sort: f.sort,
|
||||
limit: limit,
|
||||
skip: skip,
|
||||
count: count,
|
||||
success: (results,count) => {
|
||||
if (results !== undefined) {
|
||||
_c.set(
|
||||
results.map((i) => {
|
||||
var m = CloudStore._fromJSON(i, this._internal.name, this.nodeScope.modelScope);
|
||||
|
||||
return m;
|
||||
})
|
||||
);
|
||||
}
|
||||
if(count !== undefined) {
|
||||
this._internal.storageSettings.storageTotalCount = count;
|
||||
if(this.hasOutput('storageTotalCount'))
|
||||
this.flagOutputDirty('storageTotalCount');
|
||||
}
|
||||
this.setCollection(_c);
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
error: (err) => {
|
||||
this.setCollection(_c);
|
||||
this.setError(err || 'Failed to fetch.');
|
||||
}
|
||||
});
|
||||
},
|
||||
getStorageFilter: function () {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
if (storageSettings['storageFilterType'] === undefined || storageSettings['storageFilterType'] === 'simple') {
|
||||
// Create simple filter
|
||||
const _where =
|
||||
this._internal.visualFilter !== undefined
|
||||
? QueryUtils.convertVisualFilter(this._internal.visualFilter, {
|
||||
queryParameters: this._internal.queryParameters,
|
||||
collectionName: this._internal.name
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const _sort =
|
||||
this._internal.visualSorting !== undefined
|
||||
? QueryUtils.convertVisualSorting(this._internal.visualSorting)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
where: _where,
|
||||
sort: _sort
|
||||
};
|
||||
} else if (storageSettings['storageFilterType'] === 'json') {
|
||||
// JSON filter
|
||||
if (!this._internal.filterFunc) {
|
||||
try {
|
||||
var filterCode = storageSettings['storageJSONFilter'];
|
||||
|
||||
// Parse out variables
|
||||
filterCode = filterCode.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
|
||||
this._internal.filterVariables = filterCode.match(/\$[A-Za-z0-9]+/g) || [];
|
||||
|
||||
var args = ['filter', 'where', 'sort', 'Inputs']
|
||||
.concat(this._internal.filterVariables)
|
||||
.concat([filterCode]);
|
||||
this._internal.filterFunc = Function.apply(null, args);
|
||||
} catch (e) {
|
||||
this._internal.filterFunc = undefined;
|
||||
console.log('Error while parsing filter script: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._internal.filterFunc) return;
|
||||
|
||||
var _filter = {},
|
||||
_sort = [],
|
||||
_this = this;
|
||||
|
||||
// Collect filter variables
|
||||
var _filterCb = function (f) {
|
||||
_filter = QueryUtils.convertFilterOp(f, {
|
||||
collectionName: _this._internal.name,
|
||||
error: function (err) {
|
||||
_this.context.editorConnection.sendWarning(
|
||||
_this.nodeScope.componentOwner.name,
|
||||
_this.id,
|
||||
'query-collection-filter',
|
||||
{
|
||||
message: err
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
var _sortCb = function (s) {
|
||||
_sort = s;
|
||||
};
|
||||
|
||||
// Extract inputs
|
||||
const inputs = {};
|
||||
for (let key in storageSettings) {
|
||||
if (key.startsWith('storageFilterValue-'))
|
||||
inputs[key.substring('storageFilterValue-'.length)] = storageSettings[key];
|
||||
}
|
||||
|
||||
var filterFuncArgs = [_filterCb, _filterCb, _sortCb, inputs]; // One for filter, one for where
|
||||
|
||||
this._internal.filterVariables.forEach((v) => {
|
||||
filterFuncArgs.push(storageSettings['storageFilterValue-' + v.substring(1)]);
|
||||
});
|
||||
|
||||
// Run the code to get the filter
|
||||
try {
|
||||
this._internal.filterFunc.apply(this, filterFuncArgs);
|
||||
} catch (e) {
|
||||
console.log('Error while running filter script: ' + e);
|
||||
}
|
||||
|
||||
return { where: _filter, sort: _sort };
|
||||
}
|
||||
},
|
||||
getStorageLimit: function () {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
|
||||
if (!storageSettings['storageEnableLimit']) return;
|
||||
else return storageSettings['storageLimit'] || 10;
|
||||
},
|
||||
getStorageSkip: function () {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
|
||||
if (!storageSettings['storageEnableLimit']) return;
|
||||
else return storageSettings['storageSkip'] || 0;
|
||||
},
|
||||
getStorageFetchTotalCount: function() {
|
||||
const storageSettings = this._internal.storageSettings;
|
||||
|
||||
return !!storageSettings['storageEnableCount'];
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerOutput(name, {
|
||||
getter: userOutputGetter.bind(this, name)
|
||||
});
|
||||
},
|
||||
setVisualFilter: function (value) {
|
||||
this._internal.visualFilter = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
setVisualSorting: function (value) {
|
||||
this._internal.visualSorting = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
setQueryParameter: function (name, value) {
|
||||
this._internal.queryParameters[name] = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('qp-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setQueryParameter.bind(this, name.substring('qp-'.length))
|
||||
});
|
||||
|
||||
const dynamicSignals = {
|
||||
storageFetch: this.scheduleFetch.bind(this)
|
||||
};
|
||||
|
||||
if (dynamicSignals[name])
|
||||
return this.registerInput(name, {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: dynamicSignals[name]
|
||||
})
|
||||
});
|
||||
|
||||
const dynamicSetters = {
|
||||
collectionName: this.setCollectionName.bind(this),
|
||||
visualFilter: this.setVisualFilter.bind(this),
|
||||
visualSort: this.setVisualSorting.bind(this)
|
||||
};
|
||||
|
||||
if (dynamicSetters[name])
|
||||
return this.registerInput(name, {
|
||||
set: dynamicSetters[name]
|
||||
});
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userOutputGetter(name) {
|
||||
/* jshint validthis:true */
|
||||
return this._internal.storageSettings[name];
|
||||
}
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
this._internal.storageSettings[name] = value;
|
||||
|
||||
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
|
||||
}
|
||||
|
||||
const _defaultJSONQuery =
|
||||
'// Write your query script here, check out the reference documentation for examples\n' + 'where({ })\n';
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, graphModel) {
|
||||
var ports = [];
|
||||
|
||||
const dbCollections = graphModel.getMetaData('dbCollections');
|
||||
const systemCollections = graphModel.getMetaData('systemCollections');
|
||||
|
||||
const _systemClasses = [
|
||||
{ label: 'User', value: '_User' },
|
||||
{ label: 'Role', value: '_Role' }
|
||||
];
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _systemClasses.concat(
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: []
|
||||
),
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Class',
|
||||
plug: 'input',
|
||||
group: 'General'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'storageFilterType',
|
||||
type: {
|
||||
name: 'enum',
|
||||
allowEditOnly: true,
|
||||
enums: [
|
||||
{ value: 'simple', label: 'Visual' },
|
||||
{ value: 'json', label: 'Javascript' }
|
||||
]
|
||||
},
|
||||
displayName: 'Filter',
|
||||
default: 'simple',
|
||||
plug: 'input',
|
||||
group: 'General'
|
||||
});
|
||||
|
||||
// Limit
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'storageEnableLimit',
|
||||
displayName: 'Use limit'
|
||||
});
|
||||
|
||||
if (parameters['storageEnableLimit']) {
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 10,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'storageLimit',
|
||||
displayName: 'Limit'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 0,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'storageSkip',
|
||||
displayName: 'Skip'
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'input',
|
||||
group: 'Actions',
|
||||
name: 'storageFetch',
|
||||
displayName: 'Do'
|
||||
});
|
||||
|
||||
// Total Count
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
plug: 'input',
|
||||
group: 'Total Count',
|
||||
name: 'storageEnableCount',
|
||||
displayName: 'Fetch total count'
|
||||
});
|
||||
|
||||
if (parameters['storageEnableCount']) {
|
||||
ports.push({
|
||||
type: 'number',
|
||||
plug: 'output',
|
||||
group: 'General',
|
||||
name: 'storageTotalCount',
|
||||
displayName: 'Total Count'
|
||||
});
|
||||
}
|
||||
|
||||
// Simple query
|
||||
if (parameters['storageFilterType'] === undefined || parameters['storageFilterType'] === 'simple') {
|
||||
if (parameters.collectionName !== undefined) {
|
||||
var c = dbCollections && dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections) c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
const schema = JSON.parse(JSON.stringify(c.schema));
|
||||
|
||||
// Find all records that have a relation with this type
|
||||
function _findRelations(c) {
|
||||
if (c.schema !== undefined && c.schema.properties !== undefined)
|
||||
for (var key in c.schema.properties) {
|
||||
var p = c.schema.properties[key];
|
||||
if (p.type === 'Relation' && p.targetClass === parameters.collectionName) {
|
||||
if (schema.relations === undefined) schema.relations = {};
|
||||
if (schema.relations[c.name] === undefined) schema.relations[c.name] = [];
|
||||
|
||||
schema.relations[c.name].push({ property: key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbCollections && dbCollections.forEach(_findRelations);
|
||||
systemCollections && systemCollections.forEach(_findRelations);
|
||||
|
||||
ports.push({
|
||||
name: 'visualFilter',
|
||||
plug: 'input',
|
||||
type: { name: 'query-filter', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Filter',
|
||||
group: 'Filter'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'visualSort',
|
||||
plug: 'input',
|
||||
type: { name: 'query-sorting', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Sort',
|
||||
group: 'Sorting'
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters.visualFilter !== undefined) {
|
||||
// Find all input ports
|
||||
const uniqueInputs = {};
|
||||
function _collectInputs(query) {
|
||||
if (query === undefined) return;
|
||||
if (query.rules !== undefined) query.rules.forEach((r) => _collectInputs(r));
|
||||
else if (query.input !== undefined) uniqueInputs[query.input] = true;
|
||||
}
|
||||
|
||||
_collectInputs(parameters.visualFilter);
|
||||
Object.keys(uniqueInputs).forEach((input) => {
|
||||
ports.push({
|
||||
name: 'qp-' + input,
|
||||
plug: 'input',
|
||||
type: '*',
|
||||
displayName: input,
|
||||
group: 'Query Parameters'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// JSON query
|
||||
else if (parameters['storageFilterType'] === 'json') {
|
||||
ports.push({
|
||||
type: { name: 'string', allowEditOnly: true, codeeditor: 'javascript' },
|
||||
plug: 'input',
|
||||
group: 'Filter',
|
||||
name: 'storageJSONFilter',
|
||||
default: _defaultJSONQuery,
|
||||
displayName: 'Filter'
|
||||
});
|
||||
|
||||
var filter = parameters['storageJSONFilter'];
|
||||
if (filter) {
|
||||
filter = filter.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
|
||||
var variables = filter.match(/\$[A-Za-z0-9]+/g);
|
||||
|
||||
if (variables) {
|
||||
const unique = {};
|
||||
variables.forEach((v) => {
|
||||
unique[v] = true;
|
||||
});
|
||||
|
||||
Object.keys(unique).forEach((p) => {
|
||||
ports.push({
|
||||
name: 'storageFilterValue-' + p.substring(1),
|
||||
displayName: p.substring(1),
|
||||
group: 'Filter Values',
|
||||
plug: 'input',
|
||||
type: { name: '*', allowConnectionsOnly: true }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Support variables with the "Inputs."" syntax
|
||||
JavascriptNodeParser.parseAndAddPortsFromScript(filter, ports, {
|
||||
inputPrefix: 'storageFilterValue-',
|
||||
inputGroup: 'Filter Values',
|
||||
inputType: { name: '*', allowConnectionsOnly: true },
|
||||
skipOutputs: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: DbCollectionNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name.startsWith('storage') || event.name === 'visualFilter' || event.name === 'collectionName') {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
}
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.cloudservices', function (data) {
|
||||
CloudStore.instance._initCloudServices();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.DbCollection2', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('DbCollection2')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
172
packages/noodl-runtime/src/nodes/std-library/data/dbconfig.js
Normal file
172
packages/noodl-runtime/src/nodes/std-library/data/dbconfig.js
Normal file
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
const ConfigService = require('../../../api/configservice');
|
||||
|
||||
var ConfigNodeDefinition = {
|
||||
name: 'DbConfig',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/config',
|
||||
displayNodeName: 'Config',
|
||||
category: 'Cloud Services',
|
||||
usePortAsLabel: 'configKey',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
ConfigService.instance.getConfig().then((config) => {
|
||||
internal.config = config;
|
||||
if (this.hasOutput('value')) this.flagOutputDirty('value');
|
||||
});
|
||||
},
|
||||
getInspectInfo() {
|
||||
const value = this.getValue();
|
||||
|
||||
if (value === undefined) return '[No Value]';
|
||||
|
||||
return [{ type: 'value', value: value }];
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
methods: {
|
||||
getValue: function () {
|
||||
const internal = this._internal;
|
||||
if (internal.useDevValue && this.context.editorConnection && this.context.editorConnection.isRunningLocally()) {
|
||||
return internal.devValue;
|
||||
} else if (internal.config !== undefined && internal.configKey !== undefined) {
|
||||
return internal.config[internal.configKey];
|
||||
}
|
||||
},
|
||||
setInternal: function (key, value) {
|
||||
this._internal[key] = value;
|
||||
if (this.hasOutput('value')) this.flagOutputDirty('value');
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'value')
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getValue.bind(this)
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'configKey' || name === 'useDevValue' || name === 'devValue')
|
||||
return this.registerInput(name, {
|
||||
set: this.setInternal.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ConfigNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function updatePorts(node) {
|
||||
var ports = [];
|
||||
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'dbconfig-warning');
|
||||
|
||||
const configSchema = graphModel.getMetaData('dbConfigSchema');
|
||||
let valueType;
|
||||
|
||||
if (configSchema) {
|
||||
const isCloud = typeof _noodl_cloud_runtime_version !== 'undefined';
|
||||
ports.push({
|
||||
name: 'configKey',
|
||||
displayName: 'Parameter',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: Object.keys(configSchema)
|
||||
.filter((k) => isCloud || !configSchema[k].masterKeyOnly)
|
||||
.map((k) => ({ value: k, label: k })),
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (node.parameters['configKey'] !== undefined && configSchema && configSchema[node.parameters['configKey']]) {
|
||||
valueType = configSchema[node.parameters['configKey']].type;
|
||||
|
||||
if (
|
||||
valueType === 'string' ||
|
||||
valueType === 'boolean' ||
|
||||
valueType === 'number' ||
|
||||
valueType === 'object' ||
|
||||
valueType === 'array'
|
||||
) {
|
||||
ports.push({
|
||||
name: 'useDevValue',
|
||||
displayName: 'Enable',
|
||||
group: 'Local Override',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (node.parameters['useDevValue'] === true) {
|
||||
ports.push({
|
||||
name: 'devValue',
|
||||
displayName: 'Value',
|
||||
group: 'Local Override',
|
||||
type: valueType,
|
||||
plug: 'input'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (node.parameters['configKey'] !== undefined) {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'dbconfig-warning', {
|
||||
showGlobally: true,
|
||||
message: node.parameters['configKey'] + ' config parameter is missing, add it to your cloud service.'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'dbconfig-warning', {
|
||||
showGlobally: true,
|
||||
message: 'You need an active cloud service.'
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
name: 'value',
|
||||
displayName: 'Value',
|
||||
group: 'General',
|
||||
type: valueType || '*',
|
||||
plug: 'output'
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbConfigSchema', function (data) {
|
||||
ConfigService.instance.clearCache();
|
||||
updatePorts(node);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.DbConfig', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('DbConfig')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,603 @@
|
||||
'use strict';
|
||||
|
||||
const Model = require('../../../model');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
function _addBaseInfo(def, opts) {
|
||||
const _includeInputProperties = opts === undefined || opts.includeInputProperties;
|
||||
const _includeRelations = opts !== undefined && opts.includeRelations;
|
||||
|
||||
Object.assign(def.node, {
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
// Outputs
|
||||
Object.assign(def.node.outputs, {
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
scheduleOnce: function (type, cb) {
|
||||
const _this = this;
|
||||
const _type = 'hasScheduled' + type;
|
||||
if (this._internal[_type]) return;
|
||||
this._internal[_type] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this._internal[_type] = false;
|
||||
cb();
|
||||
});
|
||||
},
|
||||
checkWarningsBeforeCloudOp() {
|
||||
//clear all errors first
|
||||
this.clearWarnings();
|
||||
|
||||
if (!this._internal.collectionId) {
|
||||
this.setError('No class name specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Setup
|
||||
Object.assign(def, {
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const dbCollections = graphModel.getMetaData('dbCollections');
|
||||
const systemCollections = graphModel.getMetaData('systemCollections');
|
||||
|
||||
const _systemClasses = [
|
||||
{ label: 'User', value: '_User' },
|
||||
{ label: 'Role', value: '_Role' }
|
||||
];
|
||||
|
||||
const parameters = node.parameters;
|
||||
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
displayName: 'Class',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _systemClasses.concat(
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: []
|
||||
),
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (_includeRelations && parameters.collectionName && dbCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections)
|
||||
c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
const props = c.schema.properties;
|
||||
const enums = Object.keys(props)
|
||||
.filter((key) => props[key].type === 'Relation')
|
||||
.map((key) => ({ label: key, value: key }));
|
||||
|
||||
ports.push({
|
||||
name: 'relationProperty',
|
||||
displayName: 'Relation',
|
||||
group: 'General',
|
||||
type: { name: 'enum', enums: enums, allowEditOnly: true },
|
||||
plug: 'input'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_includeInputProperties && parameters.collectionName && dbCollections) {
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
// Fetch ports from collection keys
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections)
|
||||
c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
for (var key in props) {
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def._additionalDynamicPorts && def._additionalDynamicPorts(node, ports, graphModel);
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
_updatePorts();
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
_updatePorts();
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.' + def.node.name, function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType(def.node.name)) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _addModelId(def, opts) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
const _includeInputs = opts === undefined || opts.includeInputs;
|
||||
const _includeOutputs = opts === undefined || opts.includeOutputs;
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
if (_includeInputs) {
|
||||
Object.assign(def.node, {
|
||||
usePortAsLabel: 'collectionName'
|
||||
});
|
||||
|
||||
def.node.dynamicports = (def.node.dynamicports || []).concat([
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
]);
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Id Source',
|
||||
group: 'General',
|
||||
tooltip:
|
||||
'Choose if you want to specify the Id explicitly, \n or if you want it to be that of the current record in a repeater.',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'ModelName',
|
||||
identifierDisplayName: 'Object Ids'
|
||||
},
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
this.setModelID(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Outputs
|
||||
if (_includeOutputs) {
|
||||
Object.assign(def.node.outputs, {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
setCollectionID: function (id) {
|
||||
this._internal.collectionId = id;
|
||||
this.clearWarnings();
|
||||
},
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
this.setModel(model);
|
||||
},
|
||||
setModel: function (model) {
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'collectionName')
|
||||
this.registerInput(name, {
|
||||
set: this.setCollectionID.bind(this)
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _addInputProperties(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
Object.assign(def.node, {
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
|
||||
_def.node.initialize && _def.node.initialize.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Outputs
|
||||
Object.assign(def.node.outputs, {});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: this._setInputValue.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
_setInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _addRelationProperty(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
targetId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Target Record Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.targetModelId = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'relationProperty')
|
||||
this.registerInput(name, {
|
||||
set: this.setRelationProperty.bind(this)
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
setRelationProperty: function (value) {
|
||||
this._internal.relationProperty = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _getCurrentUser(modelScope) {
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// We are running in browser, try to find the current user
|
||||
|
||||
var _cu = localStorage['Parse/' + CloudStore.instance.appId + '/currentUser'];
|
||||
if (_cu !== undefined) {
|
||||
let cu;
|
||||
try {
|
||||
cu = JSON.parse(_cu);
|
||||
} catch (e) {}
|
||||
|
||||
return cu !== undefined ? cu.objectId : undefined;
|
||||
}
|
||||
} else {
|
||||
// Assume we are running in cloud runtime
|
||||
const request = modelScope.get('Request');
|
||||
return request.UserId;
|
||||
}
|
||||
}
|
||||
|
||||
function _addAccessControl(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
Object.assign(def.node, {
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.accessControl = {};
|
||||
|
||||
_def.node.initialize && _def.node.initialize.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
accessControl: {
|
||||
type: { name: 'proplist', autoName: 'Rule', allowEditOnly: true },
|
||||
index: 1000,
|
||||
displayName: 'Access Control Rules',
|
||||
group: 'Access Control Rules',
|
||||
set: function (value) {
|
||||
this._internal.accessControlRules = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Dynamic ports
|
||||
const _super = def._additionalDynamicPorts;
|
||||
def._additionalDynamicPorts = function (node, ports, graphModel) {
|
||||
if (node.parameters['accessControl'] !== undefined && node.parameters['accessControl'].length > 0) {
|
||||
node.parameters['accessControl'].forEach((ac) => {
|
||||
const prefix = 'acl-' + ac.id;
|
||||
// User or role?
|
||||
ports.push({
|
||||
name: prefix + '-target',
|
||||
displayName: 'Target',
|
||||
editorName: ac.label + ' | Target',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'everyone', label: 'Everyone' },
|
||||
{ value: 'role', label: 'Role' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: ac.label + ' Access Rule',
|
||||
default: 'user',
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
|
||||
if (node.parameters[prefix + '-target'] === 'role') {
|
||||
ports.push({
|
||||
name: prefix + '-role',
|
||||
displayName: 'Role',
|
||||
editorName: ac.label + ' | Role',
|
||||
group: ac.label + ' Access Rule',
|
||||
plug: 'input',
|
||||
type: 'string',
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
} else if (
|
||||
node.parameters[prefix + '-target'] === undefined ||
|
||||
node.parameters[prefix + '-target'] === 'user'
|
||||
) {
|
||||
ports.push({
|
||||
name: prefix + '-userid',
|
||||
displayName: 'User Id',
|
||||
group: ac.label + ' Access Rule',
|
||||
editorName: ac.label + ' | User Id',
|
||||
plug: 'input',
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
}
|
||||
|
||||
// Read
|
||||
ports.push({
|
||||
name: prefix + '-read',
|
||||
displayName: 'Read',
|
||||
editorName: ac.label + ' | Read',
|
||||
group: ac.label + ' Access Rule',
|
||||
plug: 'input',
|
||||
type: { name: 'boolean' },
|
||||
default: true,
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
|
||||
// Write
|
||||
ports.push({
|
||||
name: prefix + '-write',
|
||||
displayName: 'Write',
|
||||
editorName: ac.label + ' | Write',
|
||||
group: ac.label + ' Access Rule',
|
||||
plug: 'input',
|
||||
type: { name: 'boolean' },
|
||||
default: true,
|
||||
parent: 'accessControl',
|
||||
parentItemId: ac.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_super && _super(node, ports, graphModel);
|
||||
};
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('acl-'))
|
||||
this.registerInput(name, {
|
||||
set: this.setAccessControl.bind(this, name)
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
_getACL: function () {
|
||||
let acl = {};
|
||||
|
||||
function _rule(rule) {
|
||||
return {
|
||||
read: rule.read === undefined ? true : rule.read,
|
||||
write: rule.write === undefined ? true : rule.write
|
||||
};
|
||||
}
|
||||
|
||||
const currentUserId = _getCurrentUser(this.nodeScope.modelScope);
|
||||
|
||||
if (this._internal.accessControlRules !== undefined) {
|
||||
this._internal.accessControlRules.forEach((r) => {
|
||||
const rule = this._internal.accessControl[r.id];
|
||||
|
||||
if (rule === undefined) {
|
||||
const userId = currentUserId;
|
||||
if (userId !== undefined) acl[userId] = { write: true, read: true };
|
||||
} else if (rule.target === 'everyone') {
|
||||
acl['*'] = _rule(rule);
|
||||
} else if (rule.target === 'user') {
|
||||
const userId = rule.userid || currentUserId;
|
||||
acl[userId] = _rule(rule);
|
||||
} else if (rule.target === 'role') {
|
||||
acl['role:' + rule.role] = _rule(rule);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(acl).length > 0 ? acl : undefined;
|
||||
},
|
||||
setAccessControl: function (name, value) {
|
||||
const _parts = name.split('-');
|
||||
|
||||
if (this._internal.accessControl[_parts[1]] === undefined) this._internal.accessControl[_parts[1]] = {};
|
||||
this._internal.accessControl[_parts[1]][_parts[2]] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addInputProperties: _addInputProperties,
|
||||
addModelId: _addModelId,
|
||||
addBaseInfo: _addBaseInfo,
|
||||
addRelationProperty: _addRelationProperty,
|
||||
addAccessControl: _addAccessControl
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var AddDbModelRelationNodeDefinition = {
|
||||
node: {
|
||||
name: 'AddDbModelRelation',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/add-record-relation',
|
||||
displayNodeName: 'Add Record Relation',
|
||||
usePortAsLabel: 'collectionName',
|
||||
// shortDesc: "Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.",
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleAddRelation();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
relationAdded: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateInputs: function () {
|
||||
if (!this.context.editorConnection) return;
|
||||
|
||||
const _warning = (message) => {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation', {
|
||||
message
|
||||
});
|
||||
};
|
||||
|
||||
if (this._internal.collectionId === undefined) {
|
||||
_warning('No class specified');
|
||||
} else if (this._internal.relationProperty === undefined) {
|
||||
_warning('No relation property specified');
|
||||
} else if (this._internal.targetModelId === undefined) {
|
||||
_warning('No target record Id (the record to add a relation to) specified');
|
||||
} else if (this._internal.model === undefined) {
|
||||
_warning('No record Id specified (the record that should get the relation)');
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation');
|
||||
}
|
||||
},
|
||||
scheduleAddRelation: function (key) {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('StorageAddRelation', function () {
|
||||
_this.validateInputs();
|
||||
|
||||
if (!internal.model) return;
|
||||
var model = internal.model;
|
||||
|
||||
var targetModelId = internal.targetModelId;
|
||||
if (targetModelId === undefined) return;
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.modelScope).addRelation({
|
||||
collection: internal.collectionId,
|
||||
objectId: model.getId(),
|
||||
key: internal.relationProperty,
|
||||
targetObjectId: targetModelId,
|
||||
targetClass: (_this.nodeScope.modelScope || Model).get(targetModelId)._class,
|
||||
success: function (response) {
|
||||
for (var _key in response) {
|
||||
model.set(_key, response[_key]);
|
||||
}
|
||||
|
||||
// Successfully added relation
|
||||
_this.sendSignalOnOutput('relationAdded');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to add relation.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(AddDbModelRelationNodeDefinition, {
|
||||
includeRelations: true
|
||||
});
|
||||
DbModelCRUDBase.addModelId(AddDbModelRelationNodeDefinition);
|
||||
DbModelCRUDBase.addRelationProperty(AddDbModelRelationNodeDefinition);
|
||||
|
||||
module.exports = AddDbModelRelationNodeDefinition;
|
||||
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var AddDbModelRelationNodeDefinition = {
|
||||
node: {
|
||||
name: 'RemoveDbModelRelation',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/remove-record-relation',
|
||||
displayName: 'Remove Record Relation',
|
||||
usePortAsLabel: 'collectionName',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRemoveRelation();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
relationRemoved: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateInputs: function () {
|
||||
if (!this.context.editorConnection) return;
|
||||
|
||||
const _warning = (message) => {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation', {
|
||||
message
|
||||
});
|
||||
};
|
||||
|
||||
if (this._internal.collectionId === undefined) {
|
||||
_warning('No class specified');
|
||||
} else if (this._internal.relationProperty === undefined) {
|
||||
_warning('No relation property specified');
|
||||
} else if (this._internal.targetModelId === undefined) {
|
||||
_warning('No target record Id (the record to add a relation to) specified');
|
||||
} else if (this._internal.model === undefined) {
|
||||
_warning('No record Id specified (the record that should get the relation)');
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation');
|
||||
}
|
||||
},
|
||||
scheduleRemoveRelation: function (key) {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('StorageRemoveRelation', function () {
|
||||
_this.validateInputs();
|
||||
|
||||
if (!internal.model) return;
|
||||
var model = internal.model;
|
||||
|
||||
var targetModelId = internal.targetModelId;
|
||||
if (targetModelId === undefined) return;
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.modelScope).removeRelation({
|
||||
collection: internal.collectionId,
|
||||
objectId: model.getId(),
|
||||
key: internal.relationProperty,
|
||||
targetObjectId: targetModelId,
|
||||
targetClass: (_this.nodeScope.modelScope || Model).get(targetModelId)._class,
|
||||
success: function (response) {
|
||||
for (var _key in response) {
|
||||
model.set(_key, response[_key]);
|
||||
}
|
||||
|
||||
// Successfully removed relation
|
||||
_this.sendSignalOnOutput('relationRemoved');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to remove relation.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(AddDbModelRelationNodeDefinition, {
|
||||
includeRelations: true
|
||||
});
|
||||
DbModelCRUDBase.addModelId(AddDbModelRelationNodeDefinition);
|
||||
DbModelCRUDBase.addRelationProperty(AddDbModelRelationNodeDefinition);
|
||||
|
||||
module.exports = AddDbModelRelationNodeDefinition;
|
||||
@@ -0,0 +1,401 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');
|
||||
|
||||
var Model = require('../../../model');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var ModelNodeDefinition = {
|
||||
name: 'DbModel2',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/record',
|
||||
displayNodeName: 'Record',
|
||||
shortDesc: 'Database model',
|
||||
category: 'Cloud Services',
|
||||
usePortAsLabel: 'collectionName',
|
||||
color: 'data',
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
],
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
internal.relationModelIds = {};
|
||||
|
||||
var _this = this;
|
||||
this._internal.onModelChangedCallback = function (args) {
|
||||
if (_this.isInputConnected('fetch')) return;
|
||||
|
||||
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
|
||||
|
||||
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
|
||||
|
||||
_this.sendSignalOnOutput('changed');
|
||||
};
|
||||
},
|
||||
getInspectInfo() {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Record]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: model.data }
|
||||
];
|
||||
},
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
group: 'Events'
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Id Source',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId();
|
||||
// Can be passed as model as well
|
||||
else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it
|
||||
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
if (this.isInputConnected('fetch') === false) this.setModelID(value);
|
||||
else {
|
||||
this.flagOutputDirty('id');
|
||||
}
|
||||
}
|
||||
},
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCollectionID: function (id) {
|
||||
this._internal.collectionId = id;
|
||||
},
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
// this._internal.modelIsNew = false;
|
||||
this.setModel(model);
|
||||
},
|
||||
setModel: function (model) {
|
||||
if (this._internal.model)
|
||||
// Remove old listener if existing
|
||||
this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
model.on('change', this._internal.onModelChangedCallback);
|
||||
|
||||
// We have a new model, mark all outputs as dirty
|
||||
for (var key in model.data) {
|
||||
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
},
|
||||
scheduleOnce: function (type, cb) {
|
||||
const _this = this;
|
||||
const _type = 'hasScheduled' + type;
|
||||
if (this._internal[_type]) return;
|
||||
this._internal[_type] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this._internal[_type] = false;
|
||||
cb();
|
||||
});
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
|
||||
}
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
var _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('Fetch', function () {
|
||||
// Don't do fetch if no id
|
||||
if (internal.modelId === undefined || internal.modelId === '') {
|
||||
_this.setError('Missing Id.');
|
||||
return;
|
||||
}
|
||||
|
||||
const cloudstore = CloudStore.forScope(_this.nodeScope.modelScope);
|
||||
cloudstore.fetch({
|
||||
collection: internal.collectionId,
|
||||
objectId: internal.modelId, // Get the objectId part of the model id
|
||||
success: function (response) {
|
||||
var model = cloudstore._fromJSON(response, internal.collectionId);
|
||||
if (internal.model !== model) {
|
||||
// Check if we need to change model
|
||||
if (internal.model)
|
||||
// Remove old listener if existing
|
||||
internal.model.off('change', internal.onModelChangedCallback);
|
||||
|
||||
internal.model = model;
|
||||
model.on('change', internal.onModelChangedCallback);
|
||||
}
|
||||
_this.flagOutputDirty('id');
|
||||
|
||||
delete response.objectId;
|
||||
|
||||
for (var key in response) {
|
||||
if (_this.hasOutput('prop-' + key)) _this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
|
||||
_this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to fetch.');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
scheduleStore: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
if (!internal.model) return;
|
||||
|
||||
this.scheduleOnce('Store', function () {
|
||||
for (var i in internal.inputValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerOutput(name, {
|
||||
getter: userOutputGetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicSignals = {};
|
||||
|
||||
if (dynamicSignals[name])
|
||||
return this.registerInput(name, {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: dynamicSignals[name]
|
||||
})
|
||||
});
|
||||
|
||||
const dynamicSetters = {
|
||||
collectionName: this.setCollectionID.bind(this)
|
||||
};
|
||||
|
||||
if (dynamicSetters[name])
|
||||
return this.registerInput(name, {
|
||||
set: dynamicSetters[name]
|
||||
});
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userOutputGetter(name) {
|
||||
/* jshint validthis:true */
|
||||
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
|
||||
}
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
//console.log('dbmodel setter:',name,value)
|
||||
/* jshint validthis:true */
|
||||
this._internal.inputValues[name] = value;
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, graphModel) {
|
||||
var ports = [];
|
||||
|
||||
const dbCollections = graphModel.getMetaData('dbCollections');
|
||||
const systemCollections = graphModel.getMetaData('systemCollections');
|
||||
|
||||
const _systemClasses = [
|
||||
{ label: 'User', value: '_User' },
|
||||
{ label: 'Role', value: '_Role' }
|
||||
];
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
displayName: 'Class',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _systemClasses.concat(
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: []
|
||||
),
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (parameters.collectionName && dbCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c === undefined && systemCollections) c = systemCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
for (var key in props) {
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
if (p.type === 'Relation') {
|
||||
} else {
|
||||
// Other schema type ports
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: key + ' Changed',
|
||||
name: 'changed-' + key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: ModelNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.DbModel2', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('DbModel2')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var DeleteDbModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'DeleteDbModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/delete-record',
|
||||
displayNodeName: 'Delete Record',
|
||||
shortDesc:
|
||||
'Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.storageDelete();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
storageDelete: function () {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
if (!this.checkWarningsBeforeCloudOp()) return;
|
||||
|
||||
this.scheduleOnce('StorageDelete', function () {
|
||||
if (!internal.model) {
|
||||
_this.setError('Missing Record Id');
|
||||
return;
|
||||
}
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.ModelScope).delete({
|
||||
collection: internal.collectionId,
|
||||
objectId: internal.model.getId(), // Get the objectId part of the model id,
|
||||
success: function () {
|
||||
internal.model.notify('delete'); // Notify that this model has been deleted
|
||||
_this.sendSignalOnOutput('deleted');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to delete.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(DeleteDbModelPropertiedNodeDefinition, {
|
||||
includeInputProperties: false
|
||||
});
|
||||
DbModelCRUDBase.addModelId(DeleteDbModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = DeleteDbModelPropertiedNodeDefinition;
|
||||
@@ -0,0 +1,519 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('../../../../noodl-runtime');
|
||||
|
||||
const Collection = require('../../../collection'),
|
||||
Model = require('../../../model'),
|
||||
CloudStore = require('../../../api/cloudstore'),
|
||||
QueryUtils = require('../../../api/queryutils');
|
||||
|
||||
var FilterDBModelsNode = {
|
||||
name: 'FilterDBModels',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/filter-records',
|
||||
displayNodeName: 'Filter Records',
|
||||
shortDesc: 'Filter, sort and limit array',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
|
||||
this._internal.collectionChangedCallback = function () {
|
||||
if (_this.isInputConnected('filter') === true) return;
|
||||
|
||||
_this.scheduleFilter();
|
||||
};
|
||||
|
||||
this._internal.cloudStoreEvents = function (args) {
|
||||
if (_this.isInputConnected('filter') === true) return;
|
||||
|
||||
if (_this._internal.visualFilter === undefined) return;
|
||||
if (_this._internal.collection === undefined) return;
|
||||
if (args.collection !== _this._internal.collectionName) return;
|
||||
|
||||
if (args.objectId !== undefined && _this._internal.collection.contains(Model.get(args.objectId)))
|
||||
_this.scheduleFilter();
|
||||
};
|
||||
|
||||
CloudStore.instance.on('save', this._internal.cloudStoreEvents);
|
||||
|
||||
this._internal.enabled = true;
|
||||
this._internal.filterSettings = {};
|
||||
this._internal.filterParameters = {};
|
||||
// this._internal.filteredCollection = Collection.get();
|
||||
},
|
||||
getInspectInfo() {
|
||||
const collection = this._internal.filteredCollection;
|
||||
|
||||
if (!collection) {
|
||||
return { type: 'text', value: '[Not executed yet]' };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Id: ' + collection.getId()
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
value: collection.items
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
this.bindCollection(value);
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
group: 'General',
|
||||
displayName: 'Enabled',
|
||||
default: true,
|
||||
set: function (value) {
|
||||
this._internal.enabled = value;
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
type: 'signal',
|
||||
group: 'Actions',
|
||||
displayName: 'Filter',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFilter();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.filteredCollection;
|
||||
}
|
||||
},
|
||||
firstItemId: {
|
||||
type: 'string',
|
||||
displayName: 'First Record Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if (this._internal.filteredCollection !== undefined) {
|
||||
const firstItem = this._internal.filteredCollection.get(0);
|
||||
if (firstItem !== undefined) return firstItem.getId();
|
||||
}
|
||||
}
|
||||
},
|
||||
/* firstItem:{
|
||||
type: 'object',
|
||||
displayName: 'First Item',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if(this._internal.filteredCollection !== undefined) {
|
||||
return this._internal.filteredCollection.get(0);
|
||||
}
|
||||
}
|
||||
}, */
|
||||
count: {
|
||||
type: 'number',
|
||||
displayName: 'Count',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.filteredCollection ? this._internal.filteredCollection.size() : 0;
|
||||
}
|
||||
},
|
||||
modified: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Filtered'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
unbindCurrentCollection: function () {
|
||||
var collection = this._internal.collection;
|
||||
if (!collection) return;
|
||||
collection.off('change', this._internal.collectionChangedCallback);
|
||||
this._internal.collection = undefined;
|
||||
},
|
||||
bindCollection: function (collection) {
|
||||
this.unbindCurrentCollection();
|
||||
this._internal.collection = collection;
|
||||
collection && collection.on('change', this._internal.collectionChangedCallback);
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this.unbindCurrentCollection();
|
||||
|
||||
CloudStore.instance.off('save', this._internal.cloudStoreEvents);
|
||||
},
|
||||
/* getFilter: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
const options = ['case'] // List all supported options here
|
||||
|
||||
if (filterSettings['filterFilter']) {
|
||||
const filters = filterSettings['filterFilter'].split(',');
|
||||
var _filter = {};
|
||||
filters.forEach(function (f) {
|
||||
var op = '$' + (filterSettings['filterFilterOp-' + f] || 'eq');
|
||||
_filter[f] = {};
|
||||
_filter[f][op] = filterSettings['filterFilterValue-' + f];
|
||||
|
||||
options.forEach((o) => {
|
||||
var option = filterSettings['filterFilterOption-' + o + '-' + f];
|
||||
if(option) _filter[f]['$' + o] = option
|
||||
})
|
||||
})
|
||||
return _filter;
|
||||
}
|
||||
},
|
||||
getSort: function() {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (filterSettings['filterSort']) {
|
||||
const sort = filterSettings['filterSort'].split(',');
|
||||
var _sort = {};
|
||||
sort.forEach(function (s) {
|
||||
_sort[s] = filterSettings['filterSort-'+s] === 'descending'?-1:1;
|
||||
})
|
||||
return _sort;
|
||||
}
|
||||
},*/
|
||||
getLimit: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (!filterSettings['filterEnableLimit']) return;
|
||||
else return filterSettings['filterLimit'] || 10;
|
||||
},
|
||||
getSkip: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (!filterSettings['filterEnableLimit']) return;
|
||||
else return filterSettings['filterSkip'] || 0;
|
||||
},
|
||||
scheduleFilter: function () {
|
||||
if (this.collectionChangedScheduled) return;
|
||||
this.collectionChangedScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.collectionChangedScheduled = false;
|
||||
if (!this._internal.collection) return;
|
||||
|
||||
// Apply filter and write to output collection
|
||||
var filtered = [].concat(this._internal.collection.items);
|
||||
|
||||
if (this._internal.enabled) {
|
||||
const _filter = this._internal.visualFilter;
|
||||
if (_filter !== undefined) {
|
||||
var filter = QueryUtils.convertVisualFilter(_filter, {
|
||||
queryParameters: this._internal.filterParameters,
|
||||
collectionName: this._internal.collectionName
|
||||
});
|
||||
if (filter) filtered = filtered.filter((m) => QueryUtils.matchesQuery(m, filter));
|
||||
}
|
||||
|
||||
var _sort = this._internal.visualSorting;
|
||||
if (_sort !== undefined && _sort.length > 0) {
|
||||
var sort = QueryUtils.convertVisualSorting(_sort);
|
||||
}
|
||||
if (sort) filtered.sort(QueryUtils.compareObjects.bind(this, sort));
|
||||
|
||||
var skip = this.getSkip();
|
||||
if (skip) filtered = filtered.slice(skip, filtered.length);
|
||||
|
||||
var limit = this.getLimit();
|
||||
if (limit) filtered = filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
this._internal.filteredCollection = Collection.create(filtered);
|
||||
|
||||
this.sendSignalOnOutput('modified');
|
||||
this.flagOutputDirty('firstItemId');
|
||||
this.flagOutputDirty('items');
|
||||
this.flagOutputDirty('count');
|
||||
});
|
||||
},
|
||||
setCollectionName: function (name) {
|
||||
this._internal.collectionName = name;
|
||||
},
|
||||
setVisualFilter: function (value) {
|
||||
this._internal.visualFilter = value;
|
||||
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
},
|
||||
setVisualSorting: function (value) {
|
||||
this._internal.visualSorting = value;
|
||||
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
},
|
||||
setFilterParameter: function (name, value) {
|
||||
this._internal.filterParameters[name] = value;
|
||||
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'collectionName')
|
||||
return this.registerInput(name, {
|
||||
set: this.setCollectionName.bind(this)
|
||||
});
|
||||
|
||||
if (name === 'visualFilter')
|
||||
return this.registerInput(name, {
|
||||
set: this.setVisualFilter.bind(this)
|
||||
});
|
||||
|
||||
if (name === 'visualSorting')
|
||||
return this.registerInput(name, {
|
||||
set: this.setVisualSorting.bind(this)
|
||||
});
|
||||
|
||||
if (name.startsWith('fp-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setFilterParameter.bind(this, name.substring('fp-'.length))
|
||||
});
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
this._internal.filterSettings[name] = value;
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, dbCollections) {
|
||||
var ports = [];
|
||||
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums:
|
||||
dbCollections !== undefined
|
||||
? dbCollections.map((c) => {
|
||||
return { value: c.name, label: c.name };
|
||||
})
|
||||
: [],
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Class',
|
||||
plug: 'input',
|
||||
group: 'General'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterEnableLimit',
|
||||
displayName: 'Use limit'
|
||||
});
|
||||
|
||||
if (parameters['filterEnableLimit']) {
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 10,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterLimit',
|
||||
displayName: 'Limit'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 0,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterSkip',
|
||||
displayName: 'Skip'
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters.collectionName !== undefined) {
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
const schema = JSON.parse(JSON.stringify(c.schema));
|
||||
|
||||
const _supportedTypes = {
|
||||
Boolean: true,
|
||||
String: true,
|
||||
Date: true,
|
||||
Number: true,
|
||||
Pointer: true
|
||||
};
|
||||
for (var key in schema.properties) {
|
||||
if (!_supportedTypes[schema.properties[key].type]) delete schema.properties[key];
|
||||
}
|
||||
|
||||
ports.push({
|
||||
name: 'visualFilter',
|
||||
plug: 'input',
|
||||
type: { name: 'query-filter', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Filter',
|
||||
group: 'Filter'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'visualSorting',
|
||||
plug: 'input',
|
||||
type: { name: 'query-sorting', schema: schema, allowEditOnly: true },
|
||||
displayName: 'Sorting',
|
||||
group: 'Sorting'
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters.visualFilter !== undefined) {
|
||||
// Find all input ports
|
||||
const uniqueInputs = {};
|
||||
function _collectInputs(query) {
|
||||
if (query === undefined) return;
|
||||
if (query.rules !== undefined) query.rules.forEach((r) => _collectInputs(r));
|
||||
else if (query.input !== undefined) uniqueInputs[query.input] = true;
|
||||
}
|
||||
|
||||
_collectInputs(parameters.visualFilter);
|
||||
Object.keys(uniqueInputs).forEach((input) => {
|
||||
ports.push({
|
||||
name: 'fp-' + input,
|
||||
plug: 'input',
|
||||
type: '*',
|
||||
displayName: input,
|
||||
group: 'Filter Parameters'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ports.push({
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
plug: 'input',
|
||||
group: 'Filter',
|
||||
name: 'filterFilter',
|
||||
displayName: 'Filter',
|
||||
})
|
||||
|
||||
ports.push({
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
plug: 'input',
|
||||
group: 'Sort',
|
||||
name: 'filterSort',
|
||||
displayName: 'Sort',
|
||||
})
|
||||
|
||||
const filterOps = {
|
||||
"string": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' },{value: 'regex', label: 'Matches RegEx'}],
|
||||
"boolean": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' }],
|
||||
"number": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' }, { value: 'lt', label: 'Less than' }, { value: 'gt', label: 'Greater than' },
|
||||
{ value: 'gte', label: 'Greater than or equal' }, { value: 'lte', label: 'Less than or equal' }]
|
||||
}
|
||||
|
||||
if (parameters['filterFilter']) {
|
||||
var filters = parameters['filterFilter'].split(',');
|
||||
filters.forEach((f) => {
|
||||
// Type
|
||||
ports.push({
|
||||
type: { name: 'enum', enums: [{ value: 'string', label: 'String' }, { value: 'number', label: 'Number' }, { value: 'boolean', label: 'Boolean' }] },
|
||||
default: 'string',
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Type',
|
||||
editorName: f + ' filter | Type',
|
||||
name: 'filterFilterType-' + f
|
||||
})
|
||||
|
||||
var type = parameters['filterFilterType-' + f];
|
||||
|
||||
// String filter type
|
||||
ports.push({
|
||||
type: { name: 'enum', enums: filterOps[type || 'string'] },
|
||||
default: 'eq',
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Op',
|
||||
editorName: f + ' filter| Op',
|
||||
name: 'filterFilterOp-' + f
|
||||
})
|
||||
|
||||
// Case sensitivite option
|
||||
if(parameters['filterFilterOp-' + f] === 'regex') {
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Case sensitive',
|
||||
editorName: f + ' filter| Case',
|
||||
name: 'filterFilterOption-case-' + f
|
||||
})
|
||||
}
|
||||
|
||||
ports.push({
|
||||
type: type || 'string',
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Value',
|
||||
editorName: f + ' Filter Value',
|
||||
name: 'filterFilterValue-' + f
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
if (parameters['filterSort']) {
|
||||
var filters = parameters['filterSort'].split(',');
|
||||
filters.forEach((f) => {
|
||||
ports.push({
|
||||
type: { name: 'enum', enums: [{ value: 'ascending', label: 'Ascending' }, { value: 'descending', label: 'Descending' }] },
|
||||
default: 'ascending',
|
||||
plug: 'input',
|
||||
group: f + ' sort',
|
||||
displayName: 'Sort',
|
||||
editorName: f + ' sorting',
|
||||
name: 'filterSort-' + f
|
||||
})
|
||||
})
|
||||
}*/
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: FilterDBModelsNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.FilterDBModels', function (node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
CloudStore.invalidateCollections();
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,342 @@
|
||||
'use strict';
|
||||
|
||||
const Collection = require('../../../collection');
|
||||
const Model = require('../../../model');
|
||||
|
||||
function _addBaseInfo(def) {
|
||||
Object.assign(def.node, {
|
||||
category: 'Data',
|
||||
color: 'data'
|
||||
});
|
||||
}
|
||||
|
||||
function _addModelId(def, opts) {
|
||||
const _includeInputs = opts === undefined || opts.includeInputs;
|
||||
const _includeOutputs = opts === undefined || opts.includeOutputs;
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
if (_includeInputs) {
|
||||
Object.assign(def.node, {
|
||||
usePortAsLabel: 'modelId'
|
||||
});
|
||||
|
||||
def.node.dynamicports = (def.node.dynamicports || []).concat([
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
]);
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Id Source',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'ModelName',
|
||||
identifierDisplayName: 'Object Ids'
|
||||
},
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
this.setModelID(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Outputs
|
||||
if (_includeOutputs) {
|
||||
Object.assign(def.node.outputs, {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
this.setModel(model);
|
||||
},
|
||||
setModel: function (model) {
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
}
|
||||
});
|
||||
|
||||
//Inspect model
|
||||
if (!def.node.getInspectInfo) {
|
||||
def.node.getInspectInfo = function () {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Object]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: model.data }
|
||||
];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function _addInputProperties(def) {
|
||||
var _def = { node: Object.assign({}, def.node), setup: def.setup };
|
||||
var _methods = Object.assign({}, def.node.methods);
|
||||
|
||||
Object.assign(def.node, {
|
||||
inputs: def.node.inputs || {},
|
||||
outputs: def.node.outputs || {},
|
||||
methods: def.node.methods || {}
|
||||
});
|
||||
|
||||
Object.assign(def, {
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.' + def.node.name, function (node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const _types = [
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'Object', value: 'object' },
|
||||
{ label: 'Any', value: '*' }
|
||||
];
|
||||
|
||||
// Add value outputs
|
||||
var properties = node.parameters.properties;
|
||||
if (properties) {
|
||||
properties = properties ? properties.split(',') : undefined;
|
||||
for (var i in properties) {
|
||||
var p = properties[i];
|
||||
|
||||
// Property input
|
||||
ports.push({
|
||||
type: {
|
||||
name: node.parameters['type-' + p] === undefined ? '*' : node.parameters['type-' + p]
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Property Values',
|
||||
displayName: p,
|
||||
// editorName:p,
|
||||
name: 'prop-' + p
|
||||
});
|
||||
|
||||
// Property type
|
||||
ports.push({
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _types,
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Property Types',
|
||||
displayName: p,
|
||||
default: '*',
|
||||
name: 'type-' + p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'input'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
_updatePorts();
|
||||
});
|
||||
});
|
||||
|
||||
_def.setup && _def.setup(context, graphModel);
|
||||
}
|
||||
});
|
||||
|
||||
// Initilize
|
||||
Object.assign(def.node, {
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
internal.inputTypes = {};
|
||||
|
||||
_def.node.initialize && _def.node.initialize.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Outputs
|
||||
Object.assign(def.node.outputs, {});
|
||||
|
||||
// Inputs
|
||||
Object.assign(def.node.inputs, {
|
||||
properties: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
displayName: 'Properties',
|
||||
group: 'Properties to set',
|
||||
set: function (value) {}
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
Object.assign(def.node.methods, {
|
||||
_pushInputValues: function (model) {
|
||||
var internal = this._internal;
|
||||
|
||||
const _defaultValueForType = {
|
||||
boolean: false,
|
||||
string: '',
|
||||
number: 0,
|
||||
date: new Date()
|
||||
};
|
||||
|
||||
const _allKeys = {};
|
||||
for (const key in internal.inputTypes) _allKeys[key] = true;
|
||||
for (const key in internal.inputValues) _allKeys[key] = true;
|
||||
|
||||
const properties = this.model.parameters.properties || '';
|
||||
|
||||
const validProperties = properties.split(',');
|
||||
|
||||
const keysToSet = Object.keys(_allKeys).filter((key) => validProperties.indexOf(key) !== -1);
|
||||
|
||||
for (const i of keysToSet) {
|
||||
var value = internal.inputValues[i];
|
||||
|
||||
if (value !== undefined) {
|
||||
//Parse array types with string as javascript
|
||||
if (internal.inputTypes[i] !== undefined && internal.inputTypes[i] === 'array' && typeof value === 'string') {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'invalid-array-' + i
|
||||
);
|
||||
|
||||
try {
|
||||
value = eval(value); //this might be static data in the form of javascript
|
||||
} catch (e) {
|
||||
if (value.indexOf('[') !== -1 || value.indexOf('{') !== -1) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'invalid-array-' + i,
|
||||
{
|
||||
showGlobally: true,
|
||||
message: 'Invalid array<br>' + e.toString()
|
||||
}
|
||||
);
|
||||
value = [];
|
||||
} else {
|
||||
//backwards compability with how this node used to work
|
||||
value = Collection.get(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Resolve object from IDs
|
||||
if (
|
||||
internal.inputTypes[i] !== undefined &&
|
||||
internal.inputTypes[i] === 'object' &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = (this.nodeScope.modelScope || Model).get(value);
|
||||
}
|
||||
|
||||
model.set(i, value, { resolve: true });
|
||||
} else {
|
||||
model.set(i, _defaultValueForType[internal.inputTypes[i]], {
|
||||
resolve: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
scheduleStore: function () {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledStore = false;
|
||||
if (!internal.model) return;
|
||||
|
||||
this._pushInputValues(internal.model);
|
||||
|
||||
this.sendSignalOnOutput('stored');
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: this._setInputValue.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
|
||||
if (name.startsWith('type-'))
|
||||
this.registerInput(name, {
|
||||
set: this._setInputType.bind(this, name.substring('type-'.length))
|
||||
});
|
||||
|
||||
_methods && _methods.registerInputIfNeeded && _def.node.methods.registerInputIfNeeded.call(this, name);
|
||||
},
|
||||
_setInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
},
|
||||
_setInputType: function (name, value) {
|
||||
this._internal.inputTypes[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addInputProperties: _addInputProperties,
|
||||
addModelId: _addModelId,
|
||||
addBaseInfo: _addBaseInfo
|
||||
};
|
||||
277
packages/noodl-runtime/src/nodes/std-library/data/modelnode2.js
Normal file
277
packages/noodl-runtime/src/nodes/std-library/data/modelnode2.js
Normal file
@@ -0,0 +1,277 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('../../../../noodl-runtime');
|
||||
|
||||
var Model = require('../../../model');
|
||||
|
||||
var ModelNodeDefinition = {
|
||||
name: 'Model2',
|
||||
docs: 'https://docs.noodl.net/nodes/data/object/object-node',
|
||||
displayNodeName: 'Object',
|
||||
shortDesc:
|
||||
'Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'modelId',
|
||||
color: 'data',
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'idSource = explicit OR idSource NOT SET',
|
||||
inputs: ['modelId']
|
||||
}
|
||||
],
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
internal.dirtyValues = {};
|
||||
|
||||
var _this = this;
|
||||
this._internal.onModelChangedCallback = function (args) {
|
||||
if (_this.isInputConnected('fetch') === true) return;
|
||||
|
||||
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
|
||||
|
||||
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
|
||||
|
||||
_this.sendSignalOnOutput('changed');
|
||||
};
|
||||
},
|
||||
getInspectInfo() {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Object]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: model.data }
|
||||
];
|
||||
},
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
|
||||
}
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
idSource: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Specify explicitly', value: 'explicit' },
|
||||
{ label: 'From repeater', value: 'foreach' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'explicit',
|
||||
displayName: 'Get Id from',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value === 'foreach') {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
// Find closest nodescope that have a _forEachModel
|
||||
var component = this.nodeScope.componentOwner;
|
||||
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
|
||||
component = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
this.setModel(component !== undefined ? component._forEachModel : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
modelId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'ModelName',
|
||||
identifierDisplayName: 'Object Ids'
|
||||
},
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId();
|
||||
// Can be passed as model as well
|
||||
else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it
|
||||
|
||||
this._internal.modelId = value; // Wait to fetch data
|
||||
if (this.isInputConnected('fetch') === false) this.setModelID(value);
|
||||
else {
|
||||
this.flagOutputDirty('id');
|
||||
}
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
displayName: 'Properties',
|
||||
group: 'Properties',
|
||||
set: function (value) {}
|
||||
},
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleSetModel();
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
scheduleStore: function () {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledStore = false;
|
||||
if (!internal.model) return;
|
||||
|
||||
for (var i in internal.dirtyValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
internal.dirtyValues = {}; // Reset dirty values
|
||||
});
|
||||
},
|
||||
scheduleSetModel: function () {
|
||||
if (this.hasScheduledSetModel) return;
|
||||
this.hasScheduledSetModel = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledSetModel = false;
|
||||
this.setModelID(this._internal.modelId);
|
||||
});
|
||||
},
|
||||
setModelID: function (id) {
|
||||
var model = (this.nodeScope.modelScope || Model).get(id);
|
||||
this.setModel(model);
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
setModel: function (model) {
|
||||
if (this._internal.model)
|
||||
// Remove old listener if existing
|
||||
this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
|
||||
this._internal.model = model;
|
||||
this.flagOutputDirty('id');
|
||||
|
||||
// In set idSource, we are calling setModel with undefined
|
||||
if (model) {
|
||||
model.on('change', this._internal.onModelChangedCallback);
|
||||
|
||||
// We have a new model, mark all outputs as dirty
|
||||
for (var key in model.data) {
|
||||
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
}
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerOutput(name, {
|
||||
getter: userOutputGetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userOutputGetter(name) {
|
||||
/* jshint validthis:true */
|
||||
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
|
||||
}
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
this._internal.inputValues[name] = value;
|
||||
|
||||
// Store on change if no connection to store or new
|
||||
const model = this._internal.model;
|
||||
const valueChanged = model ? model.get(name) !== value : true;
|
||||
if (valueChanged) {
|
||||
this._internal.dirtyValues[name] = true;
|
||||
this.scheduleStore();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
var ports = [];
|
||||
|
||||
// Add value outputs
|
||||
var properties = parameters.properties;
|
||||
if (properties) {
|
||||
properties = properties ? properties.split(',') : undefined;
|
||||
for (var i in properties) {
|
||||
var p = properties[i];
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: '*',
|
||||
allowConnectionsOnly: true
|
||||
},
|
||||
plug: 'input/output',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + p,
|
||||
displayName: p
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: p + ' Changed',
|
||||
name: 'changed-' + p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'input/output'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: ModelNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.Model2', function (node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var NewDbModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'NewDbModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/create-new-record',
|
||||
displayName: 'Create New Record',
|
||||
usePortAsLabel: 'collectionName',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.storageInsert();
|
||||
}
|
||||
},
|
||||
sourceObjectId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Source Object Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
|
||||
this._internal.sourceObjectId = value; // Wait to fetch data
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
created: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
storageInsert: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (!this.checkWarningsBeforeCloudOp()) return;
|
||||
|
||||
this.scheduleOnce('StorageInsert', () => {
|
||||
const initValues = Object.assign(
|
||||
{},
|
||||
internal.sourceObjectId ? (this.nodeScope.modelScope || Model).get(internal.sourceObjectId).data : {},
|
||||
internal.inputValues
|
||||
);
|
||||
|
||||
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
|
||||
cloudstore.create({
|
||||
collection: internal.collectionId,
|
||||
data: initValues,
|
||||
acl: this._getACL(),
|
||||
success: (data) => {
|
||||
// Successfully created
|
||||
const m = cloudstore._fromJSON(data, internal.collectionId);
|
||||
this.setModel(m);
|
||||
this.sendSignalOnOutput('created');
|
||||
},
|
||||
error: (err) => {
|
||||
this.setError(err || 'Failed to insert.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(NewDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addModelId(NewDbModelPropertiedNodeDefinition, {
|
||||
includeOutputs: true
|
||||
});
|
||||
DbModelCRUDBase.addInputProperties(NewDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addAccessControl(NewDbModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = NewDbModelPropertiedNodeDefinition;
|
||||
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
var Model = require('../../../model');
|
||||
var ModelCRUDBase = require('./modelcrudbase');
|
||||
|
||||
var NewModelNodeDefinition = {
|
||||
node: {
|
||||
name: 'NewModel',
|
||||
docs: 'https://docs.noodl.net/nodes/data/object/create-new-object',
|
||||
displayNodeName: 'Create New Object',
|
||||
inputs: {
|
||||
new: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleNew();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
created: {
|
||||
type: 'signal',
|
||||
displayName: 'Done',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleNew: function () {
|
||||
if (this.hasScheduledNew) return;
|
||||
this.hasScheduledNew = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledNew = false;
|
||||
const newModel = (this.nodeScope.modelScope || Model).get();
|
||||
|
||||
this._pushInputValues(newModel);
|
||||
|
||||
this.setModel(newModel);
|
||||
|
||||
this.sendSignalOnOutput('created');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ModelCRUDBase.addBaseInfo(NewModelNodeDefinition);
|
||||
ModelCRUDBase.addModelId(NewModelNodeDefinition, { includeOutputs: true });
|
||||
ModelCRUDBase.addInputProperties(NewModelNodeDefinition);
|
||||
|
||||
module.exports = NewModelNodeDefinition;
|
||||
587
packages/noodl-runtime/src/nodes/std-library/data/restnode.js
Normal file
587
packages/noodl-runtime/src/nodes/std-library/data/restnode.js
Normal file
@@ -0,0 +1,587 @@
|
||||
var defaultRequestScript =
|
||||
'' +
|
||||
'//Add custom code to setup the request object before the request\n' +
|
||||
'//is made.\n' +
|
||||
'//\n' +
|
||||
'//*Request.resource contains the resource path of the request.\n' +
|
||||
'//*Request.method contains the method, GET, POST, PUT or DELETE.\n' +
|
||||
'//*Request.headers is a map where you can add additional headers.\n' +
|
||||
'//*Request.parameters is a map the parameters that will be appended\n' +
|
||||
'// to the url.\n' +
|
||||
'//*Request.content contains the content of the request as a javascript\n' +
|
||||
'// object.\n' +
|
||||
'//\n';
|
||||
('//*Inputs and *Outputs contain the inputs and outputs of the node.\n');
|
||||
|
||||
var defaultResponseScript =
|
||||
'' +
|
||||
'// Add custom code to convert the response content to outputs\n' +
|
||||
'//\n' +
|
||||
'//*Response.status The status code of the response\n' +
|
||||
'//*Response.content The content of the response as a javascript\n' +
|
||||
'// object.\n' +
|
||||
'//*Response.request The request object that resulted in the response.\n' +
|
||||
'//\n' +
|
||||
'//*Inputs and *Outputs contain the inputs and outputs of the node.\n';
|
||||
|
||||
var RestNode = {
|
||||
name: 'REST2',
|
||||
displayNodeName: 'REST',
|
||||
docs: 'https://docs.noodl.net/nodes/data/rest',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
searchTags: ['http', 'request', 'fetch'],
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
this._internal.outputValues = {};
|
||||
|
||||
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
|
||||
set: (obj, prop, value) => {
|
||||
//only send outputs when they change.
|
||||
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
|
||||
if (value !== this._internal.outputValues[prop]) {
|
||||
this.registerOutputIfNeeded('out-' + prop);
|
||||
|
||||
this._internal.outputValues[prop] = value;
|
||||
this.flagOutputDirty('out-' + prop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this._internal.self = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.inspectData
|
||||
? { type: 'value', value: this._internal.inspectData }
|
||||
: { type: 'text', value: '[Not executed yet]' };
|
||||
},
|
||||
inputs: {
|
||||
resource: {
|
||||
type: 'string',
|
||||
displayName: 'Resource',
|
||||
group: 'Request',
|
||||
default: '/',
|
||||
set: function (value) {
|
||||
this._internal.resource = value;
|
||||
}
|
||||
},
|
||||
method: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
{ label: 'DELETE', value: 'DELETE' }
|
||||
]
|
||||
},
|
||||
displayName: 'Method',
|
||||
group: 'Request',
|
||||
default: 'GET',
|
||||
set: function (value) {
|
||||
this._internal.method = value;
|
||||
}
|
||||
},
|
||||
/* scriptInputs: {
|
||||
type: { name: 'proplist', allowEditOnly: true },
|
||||
group: 'Inputs',
|
||||
set: function (value) {
|
||||
// this._internal.scriptInputs = value;
|
||||
}
|
||||
},
|
||||
scriptOutputs: {
|
||||
type: { name: 'proplist', allowEditOnly: true },
|
||||
group: 'Outputs',
|
||||
set: function (value) {
|
||||
// this._internal.scriptOutputs = value;
|
||||
}
|
||||
},*/
|
||||
requestScript: {
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
displayName: 'Request',
|
||||
default: defaultRequestScript,
|
||||
group: 'Scripts',
|
||||
set: function (script) {
|
||||
try {
|
||||
this._internal.requestFunc = new Function('Inputs', 'Outputs', 'Request', script);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
responseScript: {
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
displayName: 'Response',
|
||||
default: defaultResponseScript,
|
||||
group: 'Scripts',
|
||||
set: function (script) {
|
||||
try {
|
||||
this._internal.responseFunc = new Function('Inputs', 'Outputs', 'Response', script);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
fetch: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
type: 'signal',
|
||||
displayName: 'Cancel',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.cancelFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
canceled: {
|
||||
type: 'signal',
|
||||
displayName: 'Canceled',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
getScriptOutputValue: function (name) {
|
||||
return this._internal.outputValues[name];
|
||||
},
|
||||
setScriptInputValue: function (name, value) {
|
||||
return (this._internal.inputValues[name] = value);
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('out-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getScriptOutputValue.bind(this, name.substring('out-'.length))
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('in-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setScriptInputValue.bind(this, name.substring('in-'.length))
|
||||
});
|
||||
|
||||
/* if (name.startsWith('intype-')) return this.registerInput(name, {
|
||||
set: function() {} // Ignore input type
|
||||
})
|
||||
|
||||
if (name.startsWith('outtype-')) return this.registerInput(name, {
|
||||
set: function() {} // Ignore output type
|
||||
})*/
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledFetch) {
|
||||
internal.hasScheduledFetch = true;
|
||||
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
|
||||
}
|
||||
},
|
||||
doResponse: function (status, response, request) {
|
||||
// Process the response content with the response function
|
||||
if (this._internal.responseFunc) {
|
||||
this._internal.responseFunc.apply(this._internal.self, [
|
||||
this._internal.inputValues,
|
||||
this._internal.outputValuesProxy,
|
||||
{ status: status, content: response, request: request }
|
||||
]);
|
||||
}
|
||||
|
||||
this._internal.inspectData = { status: status, content: response };
|
||||
|
||||
// Flag status
|
||||
if (status >= 200 && status < 300) {
|
||||
this.sendSignalOnOutput('success');
|
||||
} else {
|
||||
this.sendSignalOnOutput('failure');
|
||||
}
|
||||
},
|
||||
doExternalFetch: function (request) {
|
||||
var url = request.resource;
|
||||
|
||||
// Append parameters from request as query
|
||||
if (Object.keys(request.parameters).length > 0) {
|
||||
var parameters = Object.keys(request.parameters).map(function (p) {
|
||||
return p + '=' + encodeURIComponent(request.parameters[p]);
|
||||
});
|
||||
url += '?' + parameters.join('&');
|
||||
}
|
||||
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// Running in browser
|
||||
var _this = this;
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
this._xhr = xhr;
|
||||
|
||||
xhr.open(request.method, url, true);
|
||||
for (var header in request.headers) {
|
||||
xhr.setRequestHeader(header, request.headers[header]);
|
||||
}
|
||||
xhr.onreadystatechange = function () {
|
||||
// XMLHttpRequest.DONE = 4, but torped runtime doesn't support enum
|
||||
|
||||
var sentResponse = false;
|
||||
|
||||
if (this.readyState === 4 || this.readyState === XMLHttpRequest.DONE) {
|
||||
var statusCode = this.status;
|
||||
var responseType = this.getResponseHeader('content-type');
|
||||
var rawResponse = this.response;
|
||||
delete this._xhr;
|
||||
|
||||
if (responseType) {
|
||||
responseType = responseType.toLowerCase();
|
||||
const responseData = responseType.indexOf('json') !== -1 ? JSON.parse(rawResponse) : rawResponse;
|
||||
|
||||
_this.doResponse(statusCode, responseData, request);
|
||||
sentResponse = true;
|
||||
}
|
||||
|
||||
if (sentResponse === false) {
|
||||
_this.doResponse(statusCode, rawResponse, request);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.onerror = function (e) {
|
||||
//console.log('REST: Failed to request', url);
|
||||
delete this._xhr;
|
||||
_this.sendSignalOnOutput('failure');
|
||||
};
|
||||
|
||||
xhr.onabort = function () {
|
||||
delete this._xhr;
|
||||
_this.sendSignalOnOutput('canceled');
|
||||
};
|
||||
|
||||
if (request.content) {
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify(request.content));
|
||||
} else {
|
||||
xhr.send();
|
||||
}
|
||||
} else {
|
||||
// Running in cloud runtime
|
||||
const headers = Object.assign(
|
||||
{},
|
||||
request.headers,
|
||||
request.content
|
||||
? {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
: {}
|
||||
);
|
||||
fetch(url, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: request.content ? JSON.stringify(request.content) : undefined
|
||||
})
|
||||
.then((response) => {
|
||||
const responseType = response.headers.get('content-type');
|
||||
if (responseType) {
|
||||
if (responseType.indexOf('/json') !== -1) {
|
||||
response.json().then((json) => {
|
||||
this.doResponse(response.status, json, request);
|
||||
});
|
||||
} else {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'rest-run-waring-',
|
||||
{
|
||||
message: 'REST only supports json content type in response.'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.text().then((raw) => {
|
||||
this.doResponse(response.status, raw, request);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('REST: Failed to request', url);
|
||||
console.log(e);
|
||||
this.sendSignalOnOutput('failure');
|
||||
});
|
||||
}
|
||||
},
|
||||
doFetch: function () {
|
||||
this._internal.hasScheduledFetch = false;
|
||||
|
||||
// Format resource path
|
||||
var resource = this._internal.resource;
|
||||
if (resource) {
|
||||
for (var key in this._internal.inputValues) {
|
||||
resource = resource.replace('{' + key + '}', this._internal.inputValues[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the request
|
||||
var request = {
|
||||
resource: resource,
|
||||
headers: {},
|
||||
method: this._internal.method !== undefined ? this._internal.method : 'GET',
|
||||
parameters: {}
|
||||
};
|
||||
|
||||
// Process the request content with the preprocess function
|
||||
if (this._internal.requestFunc) {
|
||||
this._internal.requestFunc.apply(this._internal.self, [
|
||||
this._internal.inputValues,
|
||||
this._internal.outputValuesProxy,
|
||||
request
|
||||
]);
|
||||
}
|
||||
|
||||
// Perform request
|
||||
this.doExternalFetch(request);
|
||||
},
|
||||
cancelFetch: function () {
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
this._xhr && this._xhr.abort();
|
||||
} else {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'rest-run-waring-', {
|
||||
message: "REST doesn't support cancel in cloud functions."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _parseScriptForErrors(script, args, name, node, context, ports) {
|
||||
// Clear run warnings if the script is edited
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-run-waring-' + name);
|
||||
|
||||
if (script === undefined) {
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-parse-waring-' + name);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new Function(...args, script);
|
||||
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-parse-waring-' + name);
|
||||
} catch (e) {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'rest-parse-waring-' + name, {
|
||||
message: '<strong>' + name + '</strong>: ' + e.message,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
|
||||
// Extract inputs and outputs
|
||||
function _exists(port) {
|
||||
return ports.find((p) => p.name === port) !== undefined;
|
||||
}
|
||||
|
||||
const scriptWithoutComments = script.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
|
||||
const inputs = scriptWithoutComments.match(/Inputs\.[A-Za-z0-9]+/g);
|
||||
if (inputs) {
|
||||
const unique = {};
|
||||
inputs.forEach((v) => {
|
||||
unique[v.substring('Inputs.'.length)] = true;
|
||||
});
|
||||
|
||||
Object.keys(unique).forEach((p) => {
|
||||
if (_exists('in-' + p)) return;
|
||||
|
||||
ports.push({
|
||||
name: 'in-' + p,
|
||||
displayName: p,
|
||||
plug: 'input',
|
||||
type: '*',
|
||||
group: 'Inputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const outputs = scriptWithoutComments.match(/Outputs\.[A-Za-z0-9]+/g);
|
||||
if (outputs) {
|
||||
const unique = {};
|
||||
outputs.forEach((v) => {
|
||||
unique[v.substring('Outputs.'.length)] = true;
|
||||
});
|
||||
|
||||
Object.keys(unique).forEach((p) => {
|
||||
if (_exists('out-' + p)) return;
|
||||
|
||||
ports.push({
|
||||
name: 'out-' + p,
|
||||
displayName: p,
|
||||
plug: 'output',
|
||||
type: '*',
|
||||
group: 'Outputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: RestNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
if (!node.parameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ports = [];
|
||||
function exists(name) {
|
||||
for (var i = 0; i < ports.length; i++) if (ports[i].name === name && ports[i].plug === 'input') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* const _typeEnums = [{value:'string',label:'String'},
|
||||
{value:'boolean',label:'Boolean'},
|
||||
{value:'number',label:'Number'},
|
||||
{value:'color',label:'Color'},
|
||||
{value:'object',label:'Object'},
|
||||
{value:'array',label:'Array'}]*/
|
||||
|
||||
// Inputs
|
||||
/* if (node.parameters['scriptInputs'] !== undefined && node.parameters['scriptInputs'].length > 0) {
|
||||
node.parameters['scriptInputs'].forEach((p) => {
|
||||
// Type for input
|
||||
ports.push({
|
||||
name: 'intype-' + p.label,
|
||||
displayName: 'Type',
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: _typeEnums, allowEditOnly: true },
|
||||
default: 'string',
|
||||
parent: 'scriptInputs',
|
||||
parentItemId: p.id
|
||||
})
|
||||
|
||||
// Default Value for input
|
||||
ports.push({
|
||||
name: 'in-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'input',
|
||||
type: node.parameters['intype-' + p.label] || 'string',
|
||||
group: 'Input Values'
|
||||
})
|
||||
})
|
||||
}*/
|
||||
|
||||
// Outputs
|
||||
/* if (node.parameters['scriptOutputs'] !== undefined && node.parameters['scriptOutputs'].length > 0) {
|
||||
node.parameters['scriptOutputs'].forEach((p) => {
|
||||
// Type for output
|
||||
ports.push({
|
||||
name: 'outtype-' + p.label,
|
||||
displayName: 'Type',
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: _typeEnums, allowEditOnly: true },
|
||||
default: 'string',
|
||||
parent: 'scriptOutputs',
|
||||
parentItemId: p.id
|
||||
})
|
||||
|
||||
// Value for output
|
||||
ports.push({
|
||||
name: 'out-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'output',
|
||||
type: node.parameters['outtype-' + p.label] || '*',
|
||||
group: 'Outputs',
|
||||
})
|
||||
})
|
||||
}*/
|
||||
|
||||
// Parse resource path inputs
|
||||
if (node.parameters.resource) {
|
||||
var inputs = node.parameters.resource.match(/\{[A-Za-z0-9_]*\}/g);
|
||||
for (var i in inputs) {
|
||||
var def = inputs[i];
|
||||
var name = def.replace('{', '').replace('}', '');
|
||||
if (exists('in-' + name)) continue;
|
||||
|
||||
ports.push({
|
||||
name: 'in-' + name,
|
||||
displayName: name,
|
||||
type: 'string',
|
||||
plug: 'input',
|
||||
group: 'Inputs'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (node.parameters['requestScript']) {
|
||||
_parseScriptForErrors(
|
||||
node.parameters['requestScript'],
|
||||
['Inputs', 'Outputs', 'Request'],
|
||||
'Request script',
|
||||
node,
|
||||
context,
|
||||
ports
|
||||
);
|
||||
}
|
||||
|
||||
if (node.parameters['responseScript']) {
|
||||
_parseScriptForErrors(
|
||||
node.parameters['responseScript'],
|
||||
['Inputs', 'Outputs', 'Response'],
|
||||
'Response script',
|
||||
node,
|
||||
context,
|
||||
ports
|
||||
);
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function () {
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.REST2', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('REST2')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
var DbModelCRUDBase = require('./dbmodelcrudbase');
|
||||
const CloudStore = require('../../../api/cloudstore');
|
||||
|
||||
var SetDbModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'SetDbModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/set-record-properties',
|
||||
displayNodeName: 'Set Record Properties',
|
||||
usePortAsLabel: 'collectionName',
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'storeType = cloud OR storeType NOT SET',
|
||||
inputs: ['storeProperties']
|
||||
}
|
||||
],
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.storeType === undefined || this._internal.storeType === 'cloud') this.scheduleSave();
|
||||
else this.scheduleStore();
|
||||
}
|
||||
},
|
||||
storeProperties: {
|
||||
displayName: 'Properties to store',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Only specified', value: 'specified' },
|
||||
{ label: 'All', value: 'all' }
|
||||
]
|
||||
},
|
||||
default: 'specified',
|
||||
set: function (value) {
|
||||
this._internal.storeProperties = value;
|
||||
}
|
||||
},
|
||||
storeType: {
|
||||
displayName: 'Store to',
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Cloud and local', value: 'cloud' },
|
||||
{ label: 'Local only', value: 'local' }
|
||||
]
|
||||
},
|
||||
default: 'cloud',
|
||||
set: function (value) {
|
||||
this._internal.storeType = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
stored: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleSave: function () {
|
||||
const _this = this;
|
||||
const internal = this._internal;
|
||||
|
||||
if (!this.checkWarningsBeforeCloudOp()) return;
|
||||
|
||||
this.scheduleOnce('StorageSave', function () {
|
||||
if (!internal.model) {
|
||||
_this.setError('Missing Record Id');
|
||||
return;
|
||||
}
|
||||
var model = internal.model;
|
||||
|
||||
for (var i in internal.inputValues) {
|
||||
model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
|
||||
CloudStore.forScope(_this.nodeScope.modelScope).save({
|
||||
collection: internal.collectionId,
|
||||
objectId: model.getId(), // Get the objectId part of the model id
|
||||
data: internal.storeProperties === 'all' ? model.data : internal.inputValues, // Only store input values by default, if not explicitly specified
|
||||
acl: _this._getACL(),
|
||||
success: function (response) {
|
||||
for (var key in response) {
|
||||
model.set(key, response[key]);
|
||||
}
|
||||
|
||||
_this.sendSignalOnOutput('stored');
|
||||
},
|
||||
error: function (err) {
|
||||
_this.setError(err || 'Failed to save.');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
scheduleStore: function () {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledStore = false;
|
||||
if (!internal.model) return;
|
||||
|
||||
for (var i in internal.inputValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
this.sendSignalOnOutput('stored');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DbModelCRUDBase.addBaseInfo(SetDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addModelId(SetDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addInputProperties(SetDbModelPropertiedNodeDefinition);
|
||||
DbModelCRUDBase.addAccessControl(SetDbModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = SetDbModelPropertiedNodeDefinition;
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
var ModelCRUDBase = require('./modelcrudbase');
|
||||
|
||||
var SetModelPropertiedNodeDefinition = {
|
||||
node: {
|
||||
name: 'SetModelProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/object/set-object-properties',
|
||||
displayNodeName: 'Set Object Properties',
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleStore();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
stored: {
|
||||
type: 'signal',
|
||||
displayName: 'Done',
|
||||
group: 'Events'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ModelCRUDBase.addBaseInfo(SetModelPropertiedNodeDefinition);
|
||||
ModelCRUDBase.addModelId(SetModelPropertiedNodeDefinition);
|
||||
ModelCRUDBase.addInputProperties(SetModelPropertiedNodeDefinition);
|
||||
|
||||
module.exports = SetModelPropertiedNodeDefinition;
|
||||
80
packages/noodl-runtime/src/nodes/std-library/datetostring.js
Normal file
80
packages/noodl-runtime/src/nodes/std-library/datetostring.js
Normal file
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const DateToStringNode = {
|
||||
name: 'Date To String',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/date-to-string',
|
||||
category: 'Utilities',
|
||||
initialize: function () {
|
||||
this._internal.formatString = '{year}-{month}-{date}';
|
||||
},
|
||||
inputs: {
|
||||
formatString: {
|
||||
displayName: 'Format',
|
||||
type: 'string',
|
||||
default: '{year}-{month}-{date}',
|
||||
set: function (value) {
|
||||
if (this._internal.formatString === value) return;
|
||||
this._internal.formatString = value;
|
||||
|
||||
if (this._internal.currentInput !== undefined) {
|
||||
this._format();
|
||||
this.flagOutputDirty('currentValue');
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
type: { name: 'date' },
|
||||
displayName: 'Date',
|
||||
set: function (value) {
|
||||
const _value = typeof value === 'string' ? new Date(value) : value;
|
||||
if (this._internal.currentInput === _value) return;
|
||||
|
||||
this._internal.currentInput = _value;
|
||||
this._format();
|
||||
this.flagOutputDirty('currentValue');
|
||||
this.sendSignalOnOutput('inputChanged');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentValue: {
|
||||
type: 'string',
|
||||
displayName: 'Date String',
|
||||
group: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.dateString;
|
||||
}
|
||||
},
|
||||
inputChanged: {
|
||||
type: 'signal',
|
||||
displayName: 'Date Changed',
|
||||
group: 'Signals'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_format() {
|
||||
const t = this._internal.currentInput;
|
||||
const format = this._internal.formatString;
|
||||
const date = ('0' + t.getDate()).slice(-2);
|
||||
const month = ('0' + (t.getMonth() + 1)).slice(-2);
|
||||
const monthShort = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(t);
|
||||
const year = t.getFullYear();
|
||||
const hours = ('0' + t.getHours()).slice(-2);
|
||||
const minutes = ('0' + t.getMinutes()).slice(-2);
|
||||
const seconds = ('0' + t.getSeconds()).slice(-2);
|
||||
|
||||
this._internal.dateString = format
|
||||
.replace(/\{date\}/g, date)
|
||||
.replace(/\{month\}/g, month)
|
||||
.replace(/\{monthShort\}/g, monthShort)
|
||||
.replace(/\{year\}/g, year)
|
||||
.replace(/\{hours\}/g, hours)
|
||||
.replace(/\{minutes\}/g, minutes)
|
||||
.replace(/\{seconds\}/g, seconds);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: DateToStringNode
|
||||
};
|
||||
359
packages/noodl-runtime/src/nodes/std-library/expression.js
Normal file
359
packages/noodl-runtime/src/nodes/std-library/expression.js
Normal file
@@ -0,0 +1,359 @@
|
||||
'use strict';
|
||||
|
||||
const difference = require('lodash.difference');
|
||||
|
||||
//const Model = require('./data/model');
|
||||
|
||||
const ExpressionNode = {
|
||||
name: 'Expression',
|
||||
docs: 'https://docs.noodl.net/nodes/math/expression',
|
||||
usePortAsLabel: 'expression',
|
||||
category: 'CustomCode',
|
||||
color: 'javascript',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'Expression'
|
||||
},
|
||||
searchTags: ['javascript'],
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.scope = {};
|
||||
internal.hasScheduledEvaluation = false;
|
||||
|
||||
internal.code = undefined;
|
||||
internal.cachedValue = 0;
|
||||
internal.currentExpression = '';
|
||||
internal.compiledFunction = undefined;
|
||||
internal.inputNames = [];
|
||||
internal.inputValues = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.cachedValue;
|
||||
},
|
||||
inputs: {
|
||||
expression: {
|
||||
group: 'General',
|
||||
inputPriority: 1,
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
displayName: 'Expression',
|
||||
set: function (value) {
|
||||
var internal = this._internal;
|
||||
internal.currentExpression = functionPreamble + 'return (' + value + ');';
|
||||
internal.compiledFunction = undefined;
|
||||
|
||||
var newInputs = parsePorts(value);
|
||||
|
||||
var inputsToAdd = difference(newInputs, internal.inputNames);
|
||||
var inputsToRemove = difference(internal.inputNames, newInputs);
|
||||
|
||||
var self = this;
|
||||
inputsToRemove.forEach(function (name) {
|
||||
self.deregisterInput(name);
|
||||
delete internal.scope[name];
|
||||
});
|
||||
|
||||
inputsToAdd.forEach(function (name) {
|
||||
if (self.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.registerInput(name, {
|
||||
set: function (value) {
|
||||
internal.scope[name] = value;
|
||||
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
|
||||
}
|
||||
});
|
||||
|
||||
internal.scope[name] = 0;
|
||||
self._inputValues[name] = 0;
|
||||
});
|
||||
|
||||
/* if(value.indexOf('Vars') !== -1 || value.indexOf('Variables') !== -1) {
|
||||
// This expression is using variables, it should listen for changes
|
||||
this._internal.onVariablesChangedCallback = (args) => {
|
||||
this._scheduleEvaluateExpression()
|
||||
}
|
||||
|
||||
Model.get('--ndl--global-variables').off('change',this._internal.onVariablesChangedCallback)
|
||||
Model.get('--ndl--global-variables').on('change',this._internal.onVariablesChangedCallback)
|
||||
}*/
|
||||
|
||||
internal.inputNames = Object.keys(internal.scope);
|
||||
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
|
||||
}
|
||||
},
|
||||
run: {
|
||||
group: 'Actions',
|
||||
displayName: 'Run',
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () {
|
||||
this._scheduleEvaluateExpression();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
group: 'Result',
|
||||
type: '*',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
if (!this._internal.currentExpression) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this._internal.cachedValue;
|
||||
}
|
||||
},
|
||||
isTrue: {
|
||||
group: 'Result',
|
||||
type: 'boolean',
|
||||
displayName: 'Is True',
|
||||
getter: function () {
|
||||
if (!this._internal.currentExpression) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!this._internal.cachedValue;
|
||||
}
|
||||
},
|
||||
isFalse: {
|
||||
group: 'Result',
|
||||
type: 'boolean',
|
||||
displayName: 'Is False',
|
||||
getter: function () {
|
||||
if (!this._internal.currentExpression) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this._internal.cachedValue;
|
||||
}
|
||||
},
|
||||
isTrueEv: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'On True'
|
||||
},
|
||||
isFalseEv: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'On False'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
registerInputIfNeeded: {
|
||||
value: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.scope[name] = 0;
|
||||
this._inputValues[name] = 0;
|
||||
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
this._internal.scope[name] = value;
|
||||
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
_scheduleEvaluateExpression: {
|
||||
value: function () {
|
||||
var internal = this._internal;
|
||||
if (internal.hasScheduledEvaluation === false) {
|
||||
internal.hasScheduledEvaluation = true;
|
||||
this.flagDirty();
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
var lastValue = internal.cachedValue;
|
||||
internal.cachedValue = this._calculateExpression();
|
||||
if (lastValue !== internal.cachedValue) {
|
||||
this.flagOutputDirty('result');
|
||||
this.flagOutputDirty('isTrue');
|
||||
this.flagOutputDirty('isFalse');
|
||||
}
|
||||
if (internal.cachedValue) this.sendSignalOnOutput('isTrueEv');
|
||||
else this.sendSignalOnOutput('isFalseEv');
|
||||
internal.hasScheduledEvaluation = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
_calculateExpression: {
|
||||
value: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
if (!internal.compiledFunction) {
|
||||
internal.compiledFunction = this._compileFunction();
|
||||
}
|
||||
for (var i = 0; i < internal.inputNames.length; ++i) {
|
||||
var inputValue = internal.scope[internal.inputNames[i]];
|
||||
internal.inputValues[i] = inputValue;
|
||||
}
|
||||
try {
|
||||
return internal.compiledFunction.apply(null, internal.inputValues);
|
||||
} catch (e) {
|
||||
console.error('Error in expression:', e.message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
_compileFunction: {
|
||||
value: function () {
|
||||
var expression = this._internal.currentExpression;
|
||||
var args = Object.keys(this._internal.scope);
|
||||
|
||||
var key = expression + args.join(' ');
|
||||
|
||||
if (compiledFunctionsCache.hasOwnProperty(key) === false) {
|
||||
args.push(expression);
|
||||
|
||||
try {
|
||||
compiledFunctionsCache[key] = construct(Function, args);
|
||||
} catch (e) {
|
||||
console.error('Failed to compile JS function', e.message);
|
||||
}
|
||||
}
|
||||
return compiledFunctionsCache[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var functionPreamble = [
|
||||
'var min = Math.min,' +
|
||||
' max = Math.max,' +
|
||||
' cos = Math.cos,' +
|
||||
' sin = Math.sin,' +
|
||||
' tan = Math.tan,' +
|
||||
' sqrt = Math.sqrt,' +
|
||||
' pi = Math.PI,' +
|
||||
' round = Math.round,' +
|
||||
' floor = Math.floor,' +
|
||||
' ceil = Math.ceil,' +
|
||||
' abs = Math.abs,' +
|
||||
' random = Math.random;'
|
||||
/* ' Vars = Variables = Noodl.Object.get("--ndl--global-variables");' */
|
||||
].join('');
|
||||
|
||||
//Since apply cannot be used on constructors (i.e. new Something) we need this hax
|
||||
//see http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
|
||||
function construct(constructor, args) {
|
||||
function F() {
|
||||
return constructor.apply(this, args);
|
||||
}
|
||||
F.prototype = constructor.prototype;
|
||||
return new F();
|
||||
}
|
||||
|
||||
var compiledFunctionsCache = {};
|
||||
|
||||
var portsToIgnore = [
|
||||
'min',
|
||||
'max',
|
||||
'cos',
|
||||
'sin',
|
||||
'tan',
|
||||
'sqrt',
|
||||
'pi',
|
||||
'round',
|
||||
'floor',
|
||||
'ceil',
|
||||
'abs',
|
||||
'random',
|
||||
'Math',
|
||||
'window',
|
||||
'document',
|
||||
'undefined',
|
||||
'Vars',
|
||||
'true',
|
||||
'false',
|
||||
'null',
|
||||
'Boolean'
|
||||
];
|
||||
|
||||
function parsePorts(expression) {
|
||||
var ports = [];
|
||||
|
||||
function addPort(name) {
|
||||
if (portsToIgnore.indexOf(name) !== -1) return;
|
||||
if (
|
||||
ports.some(function (p) {
|
||||
return p === name;
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
ports.push(name);
|
||||
}
|
||||
|
||||
// First remove all strings
|
||||
expression = expression.replace(/\"([^\"]*)\"/g, '').replace(/\'([^\']*)\'/g, '');
|
||||
|
||||
// Extract identifiers
|
||||
var identifiers = expression.matchAll(/[a-zA-Z\_\$][a-zA-Z0-9\.\_\$]*/g);
|
||||
for (const _id of identifiers) {
|
||||
var name = _id[0];
|
||||
if (name.indexOf('.') !== -1) {
|
||||
name = name.split('.')[0]; // Take first symbol on "." sequence
|
||||
}
|
||||
|
||||
addPort(name);
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, expression, editorConnection) {
|
||||
var portNames = parsePorts(expression);
|
||||
|
||||
var ports = portNames.map(function (name) {
|
||||
return {
|
||||
group: 'Parameters',
|
||||
name: name,
|
||||
type: {
|
||||
name: '*',
|
||||
editAsType: 'string'
|
||||
},
|
||||
plug: 'input'
|
||||
};
|
||||
});
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
function evalCompileWarnings(editorConnection, node) {
|
||||
try {
|
||||
new Function(node.parameters.expression);
|
||||
editorConnection.clearWarning(node.component.name, node.id, 'expression-compile-error');
|
||||
} catch (e) {
|
||||
editorConnection.sendWarning(node.component.name, node.id, 'expression-compile-error', {
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: ExpressionNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.Expression', function (node) {
|
||||
if (node.parameters.expression) {
|
||||
updatePorts(node.id, node.parameters.expression, context.editorConnection);
|
||||
evalCompileWarnings(context.editorConnection, node);
|
||||
}
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'expression') {
|
||||
updatePorts(node.id, node.parameters.expression, context.editorConnection);
|
||||
evalCompileWarnings(context.editorConnection, node);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
45
packages/noodl-runtime/src/nodes/std-library/inverter.js
Normal file
45
packages/noodl-runtime/src/nodes/std-library/inverter.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
function invert(value) {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return !value;
|
||||
}
|
||||
|
||||
const InverterNode = {
|
||||
name: 'Inverter',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/inverter',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.currentValue = undefined;
|
||||
},
|
||||
getInspectInfo() {
|
||||
return String(invert(this._internal.currentValue));
|
||||
},
|
||||
inputs: {
|
||||
value: {
|
||||
type: {
|
||||
name: 'boolean'
|
||||
},
|
||||
displayName: 'Value',
|
||||
set: function (value) {
|
||||
this._internal.currentValue = value;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
return invert(this._internal.currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: InverterNode
|
||||
};
|
||||
46
packages/noodl-runtime/src/nodes/std-library/or.js
Normal file
46
packages/noodl-runtime/src/nodes/std-library/or.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const OrNode = {
|
||||
name: 'Or',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/or',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.inputs.some(isTrue);
|
||||
},
|
||||
numberedInputs: {
|
||||
input: {
|
||||
type: 'boolean',
|
||||
displayPrefix: 'Input',
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
if (this._internal.inputs[index] === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.inputs[index] = value;
|
||||
this.flagOutputDirty('result');
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'boolean',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
return this._internal.inputs.some(isTrue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: OrNode
|
||||
};
|
||||
|
||||
function isTrue(value) {
|
||||
return value ? true : false;
|
||||
}
|
||||
325
packages/noodl-runtime/src/nodes/std-library/runtasks.js
Normal file
325
packages/noodl-runtime/src/nodes/std-library/runtasks.js
Normal file
@@ -0,0 +1,325 @@
|
||||
const { Node } = require('../../../noodl-runtime');
|
||||
const guid = require('../../guid');
|
||||
const Model = require('../../model');
|
||||
|
||||
function sendSignalOnInput(itemNode, name) {
|
||||
itemNode.queueInput(name, true); // send signal
|
||||
itemNode.queueInput(name, false);
|
||||
}
|
||||
|
||||
const RunTasksDefinition = {
|
||||
name: 'RunTasks',
|
||||
displayNodeName: 'Run Tasks',
|
||||
docs: 'https://docs.noodl.net/nodes/data/run-tasks',
|
||||
color: 'data',
|
||||
category: 'Data',
|
||||
initialize() {
|
||||
this._internal.queuedOperations = [];
|
||||
this._internal.state = 'idle';
|
||||
this._internal.maxRunningTasks = 10;
|
||||
this._internal.activeTasks = new Map(); //id => ComponentInstanceNode
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
group: 'Data',
|
||||
displayName: 'Items',
|
||||
type: 'array',
|
||||
set: function (value) {
|
||||
if (!value) return;
|
||||
if (value === this._internal.items) return;
|
||||
|
||||
this._internal.items = value;
|
||||
}
|
||||
},
|
||||
stopOnFailure: {
|
||||
group: 'General',
|
||||
displayName: 'Stop On Failure',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
set: function (value) {
|
||||
this._internal.stopOnFailure = value;
|
||||
}
|
||||
},
|
||||
maxRunningTasks: {
|
||||
group: 'General',
|
||||
displayName: 'Max Running Tasks',
|
||||
type: 'number',
|
||||
default: 10,
|
||||
set: function (value) {
|
||||
this._internal.maxRunningTasks = value;
|
||||
}
|
||||
},
|
||||
taskTemplate: {
|
||||
type: 'component',
|
||||
displayName: 'Template',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.template = value;
|
||||
}
|
||||
},
|
||||
run: {
|
||||
group: 'General',
|
||||
displayName: 'Do',
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRun();
|
||||
}
|
||||
},
|
||||
abort: {
|
||||
group: 'General',
|
||||
displayName: 'Abort',
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleAbort();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Success'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Failure'
|
||||
},
|
||||
done: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Done'
|
||||
},
|
||||
aborted: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Aborted'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleRun() {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledRun) {
|
||||
internal.hasScheduledRun = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this._queueOperation(() => {
|
||||
internal.hasScheduledRun = false;
|
||||
this.run();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
scheduleAbort() {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledAbort) {
|
||||
internal.hasScheduledAbort = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this._queueOperation(() => {
|
||||
internal.hasScheduledAbort = false;
|
||||
this.abort();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
async createTaskComponent(item) {
|
||||
const internal = this._internal;
|
||||
|
||||
const modelScope = this.nodeScope.modelScope || Model;
|
||||
const model = modelScope.create(item);
|
||||
|
||||
var itemNode = await this.nodeScope.createNode(internal.template, guid(), {
|
||||
_forEachModel: model,
|
||||
_forEachNode: this
|
||||
});
|
||||
|
||||
// This is needed to make sure any action connected to "Do"
|
||||
// is not run directly
|
||||
const _isInputConnected = itemNode.isInputConnected.bind(itemNode);
|
||||
itemNode.isInputConnected = (name) => {
|
||||
if (name === 'Do') return true;
|
||||
return _isInputConnected(name);
|
||||
};
|
||||
|
||||
// Set the Id as an input
|
||||
if (itemNode.hasInput('Id')) {
|
||||
itemNode.setInputValue('Id', model.getId());
|
||||
}
|
||||
if (itemNode.hasInput('id')) {
|
||||
itemNode.setInputValue('id', model.getId());
|
||||
}
|
||||
|
||||
// Push all other values also as inputs
|
||||
// if they exist as component inputs
|
||||
for (var inputKey in itemNode._inputs) {
|
||||
if (model.data[inputKey] !== undefined) itemNode.setInputValue(inputKey, model.data[inputKey]);
|
||||
}
|
||||
|
||||
// capture signals
|
||||
itemNode._internal.creatorCallbacks = {
|
||||
onOutputChanged: (name, value, oldValue) => {
|
||||
if ((oldValue === false || oldValue === undefined) && value === true) {
|
||||
this.itemOutputSignalTriggered(name, model, itemNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return itemNode;
|
||||
},
|
||||
async startTask(task) {
|
||||
const internal = this._internal;
|
||||
|
||||
try {
|
||||
const taskComponent = await this.createTaskComponent(task);
|
||||
internal.runningTasks++;
|
||||
sendSignalOnInput(taskComponent, 'Do');
|
||||
internal.activeTasks.set(taskComponent.id, taskComponent);
|
||||
} catch (e) {
|
||||
// Something went wrong starting the task
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
async run() {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
if (internal.state !== 'idle') {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks', {
|
||||
message: 'Cannot start when not in idle mode'
|
||||
});
|
||||
} else if (!internal.template) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks', {
|
||||
message: 'No task template specified.'
|
||||
});
|
||||
} else if (!internal.items) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks', {
|
||||
message: 'No items array provided.'
|
||||
});
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'run-tasks');
|
||||
}
|
||||
}
|
||||
|
||||
if (internal.state !== 'idle') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!internal.template) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!internal.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
internal.state = 'running';
|
||||
internal.numTasks = internal.items.length;
|
||||
internal.failedTasks = 0;
|
||||
internal.completedTasks = 0;
|
||||
internal.queuedTasks = [].concat(internal.items);
|
||||
internal.runningTasks = 0;
|
||||
|
||||
// No tasks
|
||||
if (internal.items.length === 0) {
|
||||
this.sendSignalOnOutput('success');
|
||||
internal.state = 'idle';
|
||||
}
|
||||
|
||||
// Start tasks
|
||||
for (let i = 0; i < Math.min(internal.maxRunningTasks, internal.queuedTasks.length); i++) {
|
||||
const task = internal.queuedTasks.shift();
|
||||
if (!task) break;
|
||||
|
||||
this.startTask(task);
|
||||
}
|
||||
},
|
||||
abort: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
internal.state = 'aborted';
|
||||
},
|
||||
itemOutputSignalTriggered: function (name, model, itemNode) {
|
||||
const internal = this._internal;
|
||||
|
||||
if (internal.state === 'idle') {
|
||||
// Signal while we are not running is ignored
|
||||
return;
|
||||
}
|
||||
|
||||
const checkDone = () => {
|
||||
if (internal.state === 'aborted') {
|
||||
this.sendSignalOnOutput('aborted');
|
||||
internal.state = 'idle';
|
||||
return;
|
||||
}
|
||||
|
||||
if (internal.completedTasks === internal.numTasks) {
|
||||
if (internal.failedTasks === 0) this.sendSignalOnOutput('success');
|
||||
else this.sendSignalOnOutput('failure');
|
||||
this.sendSignalOnOutput('done');
|
||||
internal.state = 'idle';
|
||||
} else {
|
||||
if (internal.stopOnFailure) {
|
||||
// Only continue if there are no failed tasks, otherwise aborted
|
||||
if (internal.failedTasks === 0) {
|
||||
internal.runningTasks++;
|
||||
const task = internal.queuedTasks.shift();
|
||||
if (task) this.startTask(task);
|
||||
} else {
|
||||
this.sendSignalOnOutput('failure');
|
||||
this.sendSignalOnOutput('aborted');
|
||||
}
|
||||
} else {
|
||||
internal.runningTasks++;
|
||||
const task = internal.queuedTasks.shift();
|
||||
if (task) this.startTask(task);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (name === 'Success') {
|
||||
internal.completedTasks++;
|
||||
internal.runningTasks--;
|
||||
checkDone();
|
||||
} else if (name === 'Failure') {
|
||||
internal.completedTasks++;
|
||||
internal.failedTasks++;
|
||||
internal.runningTasks--;
|
||||
checkDone();
|
||||
}
|
||||
|
||||
internal.activeTasks.delete(itemNode.id);
|
||||
this.nodeScope.deleteNode(itemNode);
|
||||
},
|
||||
_queueOperation(op) {
|
||||
this._internal.queuedOperations.push(op);
|
||||
this._runQueueOperations();
|
||||
},
|
||||
async _runQueueOperations() {
|
||||
if (this.runningOperations) {
|
||||
return;
|
||||
}
|
||||
this.runningOperations = true;
|
||||
|
||||
while (this._internal.queuedOperations.length) {
|
||||
const op = this._internal.queuedOperations.shift();
|
||||
await op();
|
||||
}
|
||||
|
||||
this.runningOperations = false;
|
||||
}
|
||||
},
|
||||
_deleteAllTasks() {
|
||||
for (const taskComponent of this._internal.activeTasks) {
|
||||
this.nodeScope.deleteNode(taskComponent);
|
||||
}
|
||||
this._internal.activeTasks.clear();
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this._deleteAllTasks();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: RunTasksDefinition
|
||||
};
|
||||
416
packages/noodl-runtime/src/nodes/std-library/simplejavascript.js
Normal file
416
packages/noodl-runtime/src/nodes/std-library/simplejavascript.js
Normal file
@@ -0,0 +1,416 @@
|
||||
const JavascriptNodeParser = require('../../javascriptnodeparser');
|
||||
|
||||
const SimpleJavascriptNode = {
|
||||
name: 'JavaScriptFunction',
|
||||
displayNodeName: 'Function',
|
||||
docs: 'https://docs.noodl.net/nodes/javascript/function',
|
||||
category: 'CustomCode',
|
||||
color: 'javascript',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'Script'
|
||||
},
|
||||
searchTags: ['javascript'],
|
||||
exportDynamicPorts: true,
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
this._internal.outputValues = {};
|
||||
|
||||
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
|
||||
set: (obj, prop, value) => {
|
||||
//a function node can continue running after it has been deleted. E.g. with timeouts or event listeners that hasn't been removed.
|
||||
//if the node is deleted, just do nothing
|
||||
if (this._deleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
//only send outputs when they change.
|
||||
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
|
||||
if (value !== this._internal.outputValues[prop]) {
|
||||
this.registerOutputIfNeeded('out-' + prop);
|
||||
|
||||
this._internal.outputValues[prop] = value;
|
||||
this.flagOutputDirty('out-' + prop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this._internal._this = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value: {
|
||||
inputs: this._internal.inputValues,
|
||||
outputs: this._internal.outputValues
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
scriptInputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Inputs',
|
||||
set(value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
scriptOutputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Outputs',
|
||||
set(value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
functionScript: {
|
||||
displayName: 'Script',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
group: 'General',
|
||||
set(script) {
|
||||
if (script === undefined) {
|
||||
this._internal.func = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.func = this.parseScript(script);
|
||||
|
||||
if (!this.isInputConnected('run')) this.scheduleRun();
|
||||
}
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRun();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
methods: {
|
||||
scheduleRun: function () {
|
||||
if (this.runScheduled) return;
|
||||
this.runScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.runScheduled = false;
|
||||
|
||||
if (!this._deleted) {
|
||||
this.runScript();
|
||||
}
|
||||
});
|
||||
},
|
||||
runScript: async function () {
|
||||
const func = this._internal.func;
|
||||
|
||||
if (func === undefined) return;
|
||||
|
||||
const inputs = this._internal.inputValues;
|
||||
const outputs = this._internal.outputValuesProxy;
|
||||
|
||||
// Prepare send signal functions
|
||||
for (const key in this.model.outputPorts) {
|
||||
if (this._isSignalType(key)) {
|
||||
const _sendSignal = () => {
|
||||
if (this.hasOutput(key)) this.sendSignalOnOutput(key);
|
||||
};
|
||||
this._internal.outputValues[key.substring('out-'.length)] = _sendSignal;
|
||||
this._internal.outputValues[key.substring('out-'.length)].send = _sendSignal;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await func.apply(this._internal._this, [
|
||||
inputs,
|
||||
outputs,
|
||||
JavascriptNodeParser.createNoodlAPI(this.nodeScope.modelScope),
|
||||
JavascriptNodeParser.getComponentScopeForNode(this)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'Error in JS node run code.',
|
||||
Object.getPrototypeOf(e).constructor.name + ': ' + e.message,
|
||||
e.stack
|
||||
);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('javascriptExecution')) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'js-function-run-waring',
|
||||
{
|
||||
showGlobally: true,
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
setScriptInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
|
||||
if (!this.isInputConnected('run')) this.scheduleRun();
|
||||
},
|
||||
getScriptOutputValue: function (name) {
|
||||
if (this._isSignalType(name)) {
|
||||
return undefined;
|
||||
}
|
||||
return this._internal.outputValues[name];
|
||||
},
|
||||
setScriptInputType: function (name, type) {
|
||||
this._internal.inputTypes[name] = type;
|
||||
},
|
||||
setScriptOutputType: function (name, type) {
|
||||
this._internal.outputTypes[name] = type;
|
||||
},
|
||||
parseScript: function (script) {
|
||||
var func;
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
func = new AsyncFunction(
|
||||
'Inputs',
|
||||
'Outputs',
|
||||
'Noodl',
|
||||
'Component',
|
||||
JavascriptNodeParser.getCodePrefix() + script
|
||||
);
|
||||
} catch (e) {
|
||||
console.log('Error while parsing action script: ' + e);
|
||||
}
|
||||
|
||||
return func;
|
||||
},
|
||||
_isSignalType: function (name) {
|
||||
// This will catch signals in script that may not have been delivered by the editor yet
|
||||
return this.model.outputPorts[name] && this.model.outputPorts[name].type === 'signal';
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('in-')) {
|
||||
const n = name.substring('in-'.length);
|
||||
|
||||
const input = {
|
||||
set: this.setScriptInputValue.bind(this, n)
|
||||
};
|
||||
|
||||
//make sure we register the type as well, so Noodl resolves types like color styles to an actual color
|
||||
if (this.model && this.model.parameters['intype-' + n]) {
|
||||
input.type = this.model.parameters['intype-' + n];
|
||||
}
|
||||
|
||||
this.registerInput(name, input);
|
||||
}
|
||||
|
||||
if (name.startsWith('intype-')) {
|
||||
const n = name.substring('intype-'.length);
|
||||
|
||||
this.registerInput(name, {
|
||||
set(value) {
|
||||
//make sure we register the type as well, so Noodl resolves types like color styles to an actual color
|
||||
if (this.hasInput('in' + n)) {
|
||||
this.getInput('in' + n).type = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (name.startsWith('outtype-')) {
|
||||
this.registerInput(name, {
|
||||
set() {} // Ignore
|
||||
});
|
||||
}
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('out-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getScriptOutputValue.bind(this, name.substring('out-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _parseScriptForErrorsAndPorts(script, name, node, context, ports) {
|
||||
// Clear run warnings if the script is edited
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-run-waring');
|
||||
|
||||
if (script === undefined) {
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-parse-waring');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
new AsyncFunction('Inputs', 'Outputs', 'Noodl', 'Component', script);
|
||||
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-parse-waring');
|
||||
} catch (e) {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'js-function-parse-waring', {
|
||||
showGlobally: true,
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
|
||||
JavascriptNodeParser.parseAndAddPortsFromScript(script, ports, {
|
||||
inputPrefix: 'in-',
|
||||
outputPrefix: 'out-'
|
||||
});
|
||||
}
|
||||
|
||||
const inputTypeEnums = [
|
||||
{
|
||||
value: 'string',
|
||||
label: 'String'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
label: 'Boolean'
|
||||
},
|
||||
{
|
||||
value: 'number',
|
||||
label: 'Number'
|
||||
},
|
||||
{
|
||||
value: 'object',
|
||||
label: 'Object'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: 'Date'
|
||||
},
|
||||
{
|
||||
value: 'array',
|
||||
label: 'Array'
|
||||
},
|
||||
{
|
||||
value: 'color',
|
||||
label: 'Color'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
node: SimpleJavascriptNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const _outputTypeEnums = inputTypeEnums.concat([
|
||||
{
|
||||
value: 'signal',
|
||||
label: 'Signal'
|
||||
}
|
||||
]);
|
||||
|
||||
// Outputs
|
||||
if (node.parameters['scriptOutputs'] !== undefined && node.parameters['scriptOutputs'].length > 0) {
|
||||
node.parameters['scriptOutputs'].forEach((p) => {
|
||||
// Type for output
|
||||
ports.push({
|
||||
name: 'outtype-' + p.label,
|
||||
displayName: 'Type',
|
||||
editorName: p.label + ' | Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _outputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptOutputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Value for output
|
||||
ports.push({
|
||||
name: 'out-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'output',
|
||||
type: node.parameters['outtype-' + p.label] || '*',
|
||||
group: 'Outputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inputs
|
||||
if (node.parameters['scriptInputs'] !== undefined && node.parameters['scriptInputs'].length > 0) {
|
||||
node.parameters['scriptInputs'].forEach((p) => {
|
||||
// Type for input
|
||||
ports.push({
|
||||
name: 'intype-' + p.label,
|
||||
displayName: 'Type',
|
||||
editorName: p.label + ' | Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: inputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptInputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Default Value for input
|
||||
ports.push({
|
||||
name: 'in-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'input',
|
||||
type: node.parameters['intype-' + p.label] || 'string',
|
||||
group: 'Inputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_parseScriptForErrorsAndPorts(node.parameters['functionScript'], 'Script ', node, context, ports);
|
||||
|
||||
// Push output ports that are signals directly to the model, it's needed by the initial run of
|
||||
// the script function
|
||||
ports.forEach((p) => {
|
||||
if (p.type === 'signal' && p.plug === 'output') {
|
||||
node.outputPorts[p.name] = p;
|
||||
}
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.JavaScriptFunction', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('JavaScriptFunction')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
135
packages/noodl-runtime/src/nodes/std-library/stringformat.js
Normal file
135
packages/noodl-runtime/src/nodes/std-library/stringformat.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const StringFormatDefinition = {
|
||||
name: 'String Format',
|
||||
docs: 'https://docs.noodl.net/nodes/string-manipulation/string-format',
|
||||
category: 'String Manipulation',
|
||||
initialize() {
|
||||
const internal = this._internal;
|
||||
internal.format = '';
|
||||
internal.cachedResult = '';
|
||||
internal.resultDirty = false;
|
||||
internal.inputValues = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this.formatValue();
|
||||
},
|
||||
inputs: {
|
||||
format: {
|
||||
type: { name: 'string', multiline: true },
|
||||
displayName: 'Format',
|
||||
set(value) {
|
||||
if (this._internal.format === value) return;
|
||||
|
||||
this._internal.format = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.scheduleFormat();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
formatted: {
|
||||
type: 'string',
|
||||
displayName: 'Formatted',
|
||||
get() {
|
||||
return this.formatValue();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatValue() {
|
||||
var internal = this._internal;
|
||||
|
||||
if (internal.resultDirty) {
|
||||
var formatted = internal.format;
|
||||
|
||||
var matches = internal.format.match(/\{[A-Za-z0-9_]*\}/g);
|
||||
var inputs = [];
|
||||
if (matches) {
|
||||
inputs = matches.map(function (name) {
|
||||
return name.replace('{', '').replace('}', '');
|
||||
});
|
||||
}
|
||||
|
||||
inputs.forEach(function (name) {
|
||||
var v = internal.inputValues[name];
|
||||
formatted = formatted.replace('{' + name + '}', v !== undefined ? v : '');
|
||||
});
|
||||
|
||||
internal.cachedResult = formatted;
|
||||
internal.resultDirty = false;
|
||||
}
|
||||
|
||||
return internal.cachedResult;
|
||||
},
|
||||
registerInputIfNeeded(name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
},
|
||||
scheduleFormat() {
|
||||
if (this.formatScheduled) return;
|
||||
|
||||
this.formatScheduled = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.formatValue();
|
||||
this.flagOutputDirty('formatted');
|
||||
this.formatScheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
if (this._internal.inputValues[name] === value) return;
|
||||
|
||||
this._internal.inputValues[name] = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.scheduleFormat();
|
||||
}
|
||||
|
||||
function updatePorts(id, format, editorConnection) {
|
||||
var inputs = format.match(/\{[A-Za-z0-9_]*\}/g) || [];
|
||||
var portsNames = inputs.map(function (def) {
|
||||
return def.replace('{', '').replace('}', '');
|
||||
});
|
||||
|
||||
var ports = portsNames
|
||||
//get unique names
|
||||
.filter(function (value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
})
|
||||
//and map names to ports
|
||||
.map(function (name) {
|
||||
return {
|
||||
name: name,
|
||||
type: 'string',
|
||||
plug: 'input'
|
||||
};
|
||||
});
|
||||
|
||||
editorConnection.sendDynamicPorts(id, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: StringFormatDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.String Format', function (node) {
|
||||
if (node.parameters.format) {
|
||||
updatePorts(node.id, node.parameters.format, context.editorConnection);
|
||||
}
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'format') {
|
||||
updatePorts(node.id, node.parameters.format, context.editorConnection);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
94
packages/noodl-runtime/src/nodes/std-library/stringmapper.js
Normal file
94
packages/noodl-runtime/src/nodes/std-library/stringmapper.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const StringMapperNode = {
|
||||
name: 'String Mapper',
|
||||
docs: 'https://docs.noodl.net/nodes/string-manipulation/string-mapper',
|
||||
category: 'Utilities',
|
||||
initialize: function () {
|
||||
this._internal.inputs = [];
|
||||
this._internal.mappings = [];
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.mappedString;
|
||||
},
|
||||
numberedInputs: {
|
||||
input: {
|
||||
type: 'string',
|
||||
displayPrefix: 'Input',
|
||||
group: 'Inputs',
|
||||
index: 10,
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
value = value === undefined ? '' : value.toString();
|
||||
this._internal.inputs[index] = value;
|
||||
this.scheduleMapping();
|
||||
};
|
||||
}
|
||||
},
|
||||
output: {
|
||||
type: 'string',
|
||||
displayPrefix: 'Mapping',
|
||||
index: 1001,
|
||||
group: 'Mappings',
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
value = value === undefined ? '' : value.toString();
|
||||
this._internal.mappings[index] = value;
|
||||
this.scheduleMapping();
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
inputString: {
|
||||
type: {
|
||||
name: 'string'
|
||||
},
|
||||
index: 1,
|
||||
displayName: 'Input String',
|
||||
set: function (value) {
|
||||
this._internal.currentInputString = value !== undefined ? value.toString() : undefined;
|
||||
this.scheduleMapping();
|
||||
}
|
||||
},
|
||||
defaultMapping: {
|
||||
type: 'string',
|
||||
displayName: 'Default',
|
||||
index: 1000,
|
||||
group: 'Mappings',
|
||||
set: function (value) {
|
||||
this._internal.defaultMapping = value;
|
||||
this.scheduleMapping();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
mappedString: {
|
||||
type: 'string',
|
||||
displayName: 'Mapped String',
|
||||
group: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.mappedString;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
doMapping: function () {
|
||||
this._internal.hasScheduledFetch = false;
|
||||
var idx = this._internal.inputs.indexOf(this._internal.currentInputString);
|
||||
if (idx !== -1) this._internal.mappedString = this._internal.mappings[idx];
|
||||
else this._internal.mappedString = this._internal.defaultMapping;
|
||||
|
||||
this.flagOutputDirty('mappedString');
|
||||
},
|
||||
scheduleMapping: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledFetch) {
|
||||
internal.hasScheduledFetch = true;
|
||||
this.scheduleAfterInputsHaveUpdated(this.doMapping.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: StringMapperNode
|
||||
};
|
||||
76
packages/noodl-runtime/src/nodes/std-library/substring.js
Normal file
76
packages/noodl-runtime/src/nodes/std-library/substring.js
Normal file
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
const SubStringNode = {
|
||||
name: 'Substring',
|
||||
docs: 'https://docs.noodl.net/nodes/string-manipulation/substring',
|
||||
category: 'String Manipulation',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.startIndex = 0;
|
||||
internal.endIndex = -1;
|
||||
internal.cachedResult = '';
|
||||
internal.inputString = '';
|
||||
internal.resultDirty = false;
|
||||
},
|
||||
inputs: {
|
||||
start: {
|
||||
type: 'number',
|
||||
displayName: 'Start',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.startIndex = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
},
|
||||
end: {
|
||||
type: 'number',
|
||||
displayName: 'End',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.endIndex = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
},
|
||||
string: {
|
||||
type: {
|
||||
name: 'string'
|
||||
},
|
||||
displayName: 'String',
|
||||
default: '',
|
||||
set: function (value) {
|
||||
value = value.toString();
|
||||
this._internal.inputString = value;
|
||||
this._internal.resultDirty = true;
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'string',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
if (internal.resultDirty) {
|
||||
if (internal.endIndex === -1) {
|
||||
internal.cachedResult = internal.inputString.substr(internal.startIndex);
|
||||
} else {
|
||||
internal.cachedResult = internal.inputString.substr(
|
||||
internal.startIndex,
|
||||
internal.endIndex - internal.startIndex
|
||||
);
|
||||
}
|
||||
internal.resultDirty = false;
|
||||
}
|
||||
return internal.cachedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: SubStringNode
|
||||
};
|
||||
41
packages/noodl-runtime/src/nodes/std-library/uniqueid.js
Normal file
41
packages/noodl-runtime/src/nodes/std-library/uniqueid.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const Model = require('../../model');
|
||||
|
||||
const UniqueIdNode = {
|
||||
name: 'Unique Id',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/unique-id',
|
||||
category: 'String Manipulation',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.guid = Model.guid();
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.guid;
|
||||
},
|
||||
inputs: {
|
||||
new: {
|
||||
displayName: 'New',
|
||||
valueChangedToTrue: function () {
|
||||
var internal = this._internal;
|
||||
internal.guid = Model.guid();
|
||||
this.flagOutputDirty('guid');
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
guid: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
getter: function () {
|
||||
var internal = this._internal;
|
||||
return internal.guid;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: UniqueIdNode
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
'use strict';
|
||||
|
||||
const NoodlRuntime = require('../../../../noodl-runtime');
|
||||
|
||||
var SetUserPropertiesNodeDefinition = {
|
||||
name: 'net.noodl.user.SetUserProperties',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/set-user-properties',
|
||||
displayNodeName: 'Set User Properties',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.userProperties = {};
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
store: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleStore();
|
||||
}
|
||||
},
|
||||
email: {
|
||||
displayName: 'Email',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.email = value;
|
||||
}
|
||||
},
|
||||
username: {
|
||||
displayName: 'Username',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.username = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'user-set-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'user-set-warning');
|
||||
}
|
||||
},
|
||||
scheduleStore: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.storeScheduled === true) return;
|
||||
this.storeScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.storeScheduled = false;
|
||||
|
||||
const UserService = NoodlRuntime.Services.UserService;
|
||||
UserService.forScope(this.nodeScope.modelScope).setUserProperties({
|
||||
email: this._internal.email,
|
||||
username: this._internal.username,
|
||||
properties: internal.userProperties,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
setUserProperty: function (name, value) {
|
||||
this._internal.userProperties[name] = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setUserProperty.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, systemCollections) {
|
||||
var ports = [];
|
||||
|
||||
if (systemCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = systemCollections.find((c) => c.name === '_User');
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
|
||||
const _ignoreKeys =
|
||||
typeof _noodl_cloud_runtime_version === 'undefined'
|
||||
? ['authData', 'createdAt', 'updatedAt', 'email', 'username', 'emailVerified', 'password']
|
||||
: ['authData', 'createdAt', 'updatedAt', 'email', 'username'];
|
||||
|
||||
for (var key in props) {
|
||||
if (_ignoreKeys.indexOf(key) !== -1) continue;
|
||||
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
if (p.type === 'Relation') {
|
||||
} else {
|
||||
// Other schema type ports
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: SetUserPropertiesNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.net.noodl.user.SetUserProperties', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('net.noodl.user.SetUserProperties')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
338
packages/noodl-runtime/src/nodes/std-library/user/user.js
Normal file
338
packages/noodl-runtime/src/nodes/std-library/user/user.js
Normal file
@@ -0,0 +1,338 @@
|
||||
'use strict';
|
||||
|
||||
const NoodlRuntime = require('../../../../noodl-runtime');
|
||||
const { Node } = require('../../../../noodl-runtime');
|
||||
|
||||
var UserNodeDefinition = {
|
||||
name: 'net.noodl.user.User',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/user-node',
|
||||
displayNodeName: 'User',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
this._internal.onModelChangedCallback = function (args) {
|
||||
if (_this.isInputConnected('fetch')) return;
|
||||
|
||||
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
|
||||
|
||||
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
|
||||
|
||||
_this.sendSignalOnOutput('changed');
|
||||
};
|
||||
|
||||
const userService = NoodlRuntime.Services.UserService.forScope(this.nodeScope.modelScope);
|
||||
|
||||
this.setUserModel(userService.current);
|
||||
userService.on('loggedIn', () => {
|
||||
this.setUserModel(userService.current);
|
||||
|
||||
if (this.hasOutput('loggedIn')) this.sendSignalOnOutput('loggedIn');
|
||||
});
|
||||
|
||||
userService.on('sessionGained', () => {
|
||||
this.setUserModel(userService.current);
|
||||
});
|
||||
|
||||
userService.on('loggedOut', () => {
|
||||
this.setUserModel(undefined);
|
||||
if (this.hasOutput('loggedOut')) this.sendSignalOnOutput('loggedOut');
|
||||
});
|
||||
|
||||
userService.on('sessionLost', () => {
|
||||
this.setUserModel(undefined);
|
||||
if (this.hasOutput('sessionLost')) this.sendSignalOnOutput('sessionLost');
|
||||
});
|
||||
},
|
||||
getInspectInfo() {
|
||||
const model = this._internal.model;
|
||||
if (!model) return '[No Model]';
|
||||
|
||||
return [
|
||||
{ type: 'text', value: 'Id: ' + model.getId() },
|
||||
{ type: 'value', value: this._internal.model.data }
|
||||
];
|
||||
},
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined ? this._internal.model.getId() : undefined;
|
||||
}
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
group: 'Events'
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
displayName: 'Username',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined ? this._internal.model.get('username') : undefined;
|
||||
}
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
displayName: 'Email',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined ? this._internal.model.get('email') : undefined;
|
||||
}
|
||||
},
|
||||
authenticated: {
|
||||
type: 'boolean',
|
||||
displayName: 'Authenticated',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.model !== undefined;
|
||||
}
|
||||
}
|
||||
/* loggedIn:{
|
||||
type:'signal',
|
||||
displayName:'Logged In',
|
||||
group:'Events'
|
||||
},
|
||||
loggedOut:{
|
||||
type:'signal',
|
||||
displayName:'Logged Out',
|
||||
group:'Events'
|
||||
},
|
||||
sessionLost:{
|
||||
type:'signal',
|
||||
displayName:'Session Lost',
|
||||
group:'Events'
|
||||
}, */
|
||||
},
|
||||
inputs: {
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
},
|
||||
scheduleOnce: function (type, cb) {
|
||||
const _this = this;
|
||||
const _type = 'hasScheduled' + type;
|
||||
if (this._internal[_type]) return;
|
||||
this._internal[_type] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this._internal[_type] = false;
|
||||
cb();
|
||||
});
|
||||
},
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'user-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'user-warning');
|
||||
}
|
||||
},
|
||||
setUserModel(model) {
|
||||
const internal = this._internal;
|
||||
|
||||
if (internal.model !== model) {
|
||||
// Check if we need to change model
|
||||
if (internal.model)
|
||||
// Remove old listener if existing
|
||||
internal.model.off('change', internal.onModelChangedCallback);
|
||||
|
||||
internal.model = model;
|
||||
if (model) model.on('change', internal.onModelChangedCallback);
|
||||
}
|
||||
this.flagOutputDirty('id');
|
||||
this.flagOutputDirty('authenticated');
|
||||
this.flagOutputDirty('email');
|
||||
this.flagOutputDirty('username');
|
||||
|
||||
// Notify all properties changed
|
||||
if (model)
|
||||
for (var key in model.data) {
|
||||
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
|
||||
}
|
||||
},
|
||||
scheduleFetch: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
this.scheduleOnce('Fetch', () => {
|
||||
const userService = NoodlRuntime.Services.UserService.forScope(this.nodeScope.modelScope);
|
||||
userService.fetchCurrentUser({
|
||||
success: (response) => {
|
||||
this.setUserModel(userService.current);
|
||||
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
error: (err) => {
|
||||
this.setError(err || 'Failed to fetch.');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'loggedOut' || name === 'loggedIn' || name === 'sessionLost') {
|
||||
this.registerOutput(name, {
|
||||
getter: () => {} /* No getter needed, signal */
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
this.registerOutput(name, {
|
||||
getter: this.getUserProperty.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
},
|
||||
getUserProperty: function (name) {
|
||||
return this._internal.model !== undefined ? this._internal.model.get(name) : undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, systemCollections) {
|
||||
var ports = [];
|
||||
|
||||
if (systemCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = systemCollections.find((c) => c.name === '_User');
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
const _ignoreKeys = ['authData', 'password', 'username', 'email'];
|
||||
for (var key in props) {
|
||||
if (_ignoreKeys.indexOf(key) !== -1) continue;
|
||||
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
if (p.type === 'Relation') {
|
||||
} else {
|
||||
// Other schema type ports
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: key + ' Changed',
|
||||
name: 'changed-' + key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
||||
// On the client we have some extra outputs
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
name: 'loggedIn',
|
||||
type: 'signal',
|
||||
displayName: 'Logged In',
|
||||
group: 'Events'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
name: 'loggedOut',
|
||||
type: 'signal',
|
||||
displayName: 'Logged Out',
|
||||
group: 'Events'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
name: 'sessionLost',
|
||||
type: 'signal',
|
||||
displayName: 'Session Lost',
|
||||
group: 'Events'
|
||||
});
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: UserNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.net.noodl.user.User', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('net.noodl.user.User')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const VariableBase = require('./variablebase');
|
||||
|
||||
const BooleanNode = VariableBase.createDefinition({
|
||||
name: 'Boolean',
|
||||
docs: 'https://docs.noodl.net/nodes/data/boolean',
|
||||
startValue: false,
|
||||
type: {
|
||||
name: 'boolean'
|
||||
},
|
||||
cast: function (value) {
|
||||
return Boolean(value);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
node: BooleanNode
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const VariableBase = require('./variablebase');
|
||||
|
||||
const NumberNode = VariableBase.createDefinition({
|
||||
name: 'Number',
|
||||
docs: 'https://docs.noodl.net/nodes/data/number',
|
||||
startValue: 0,
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'value'
|
||||
},
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
cast: function (value) {
|
||||
return Number(value);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
node: NumberNode
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const VariableBase = require('./variablebase');
|
||||
const { NodeDefinition } = require('../../../../noodl-runtime');
|
||||
|
||||
const StringNode = VariableBase.createDefinition({
|
||||
name: 'String',
|
||||
docs: 'https://docs.noodl.net/nodes/data/string',
|
||||
shortDesc: 'Contains a string (text).',
|
||||
startValue: '',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'value'
|
||||
},
|
||||
type: {
|
||||
name: 'string'
|
||||
},
|
||||
cast: function (value) {
|
||||
return String(value);
|
||||
},
|
||||
onChanged: function () {
|
||||
this.flagOutputDirty('length');
|
||||
}
|
||||
});
|
||||
|
||||
NodeDefinition.extend(StringNode, {
|
||||
usePortAsLabel: 'value',
|
||||
portLabelTruncationMode: 'length',
|
||||
outputs: {
|
||||
length: {
|
||||
type: 'number',
|
||||
displayName: 'Length',
|
||||
getter: function () {
|
||||
return this._internal.currentValue.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
node: StringNode
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
function createDefinition(args) {
|
||||
return {
|
||||
name: args.name,
|
||||
docs: args.docs,
|
||||
shortDesc: args.shortDesc,
|
||||
nodeDoubleClickAction: args.nodeDoubleClickAction,
|
||||
category: 'Variables',
|
||||
initialize: function () {
|
||||
this._internal.currentValue = args.startValue;
|
||||
this._internal.latestValue = 0;
|
||||
},
|
||||
getInspectInfo() {
|
||||
const type = args.type.name === 'color' ? 'color' : 'text';
|
||||
return [{ type, value: this._internal.currentValue }];
|
||||
},
|
||||
inputs: {
|
||||
value: {
|
||||
type: args.type,
|
||||
displayName: 'Value',
|
||||
default: args.startValue,
|
||||
set: function (value) {
|
||||
if (this.isInputConnected('saveValue') === false) {
|
||||
this.setValueTo(value);
|
||||
} else {
|
||||
this._internal.latestValue = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
saveValue: {
|
||||
displayName: 'Set',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
this.setValueTo(this._internal.latestValue);
|
||||
this.sendSignalOnOutput('stored');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
savedValue: {
|
||||
type: args.type.name,
|
||||
displayName: 'Value',
|
||||
getter: function () {
|
||||
return this._internal.currentValue;
|
||||
}
|
||||
},
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed'
|
||||
},
|
||||
stored: {
|
||||
type: 'signal',
|
||||
displayName: 'Stored'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setValueTo: function (value) {
|
||||
value = args.cast(value);
|
||||
const changed = this._internal.currentValue !== value;
|
||||
this._internal.currentValue = value;
|
||||
|
||||
if (changed) {
|
||||
this.flagOutputDirty('savedValue');
|
||||
this.sendSignalOnOutput('changed');
|
||||
args.onChanged && args.onChanged.call(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDefinition: createDefinition
|
||||
};
|
||||
Reference in New Issue
Block a user