Initial commit

Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
const CloudFile = require('../../../api/cloudfile');
const CloudFileNode = {
name: 'Cloud File',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/cloud-file',
category: 'Cloud Services',
color: 'data',
getInspectInfo() {
return this._internal.cloudFile && this._internal.cloudFile.getUrl();
},
outputs: {
url: {
type: 'string',
displayName: 'URL',
group: 'General',
get() {
return this._internal.cloudFile && this._internal.cloudFile.getUrl();
}
},
name: {
type: 'string',
displayName: 'Name',
group: 'General',
get() {
if (!this._internal.cloudFile) return;
//parse prefixes the file with a guid_
//remove it so the name is the same as the original file name
const n = this._internal.cloudFile.getName().split('_');
return n.length === 1 ? n[0] : n.slice(1).join('_');
}
}
},
inputs: {
file: {
type: 'cloudfile',
displayName: 'Cloud File',
group: 'General',
set(value) {
if (value instanceof CloudFile === false) {
return;
}
this._internal.cloudFile = value;
this.flagOutputDirty('name');
this.flagOutputDirty('url');
}
}
}
};
module.exports = {
node: CloudFileNode
};

View File

@@ -0,0 +1,722 @@
const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');
const Model = require('../../../model'),
Collection = require('../../../collection'),
CloudStore = require('../../../api/cloudstore'),
JavascriptNodeParser = require('../../../javascriptnodeparser'),
QueryUtils = require('../../../api/queryutils');
var DbCollectionNode = {
name: 'DbCollection2',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/query-records',
displayName: 'Query Records',
/* shortDesc: "A database collection.",*/
category: 'Cloud Services',
usePortAsLabel: 'collectionName',
color: 'data',
initialize: function () {
var _this = this;
this._internal.queryParameters = {};
var collectionChangedScheduled = false;
this._internal.collectionChangedCallback = function () {
//this can be called multiple times when adding/removing more than one item
//so optimize by only updating outputs once
if (collectionChangedScheduled) return;
collectionChangedScheduled = true;
_this.scheduleAfterInputsHaveUpdated(function () {
_this.flagOutputDirty('count');
_this.flagOutputDirty('firstItemId');
collectionChangedScheduled = false;
});
};
this._internal.cloudStoreEvents = function (args) {
if (_this.isInputConnected('storageFetch') === true) return;
if (_this._internal.collection === undefined) return;
if (args.collection !== _this._internal.name) return;
function _addModelAtCorrectIndex(m) {
if (_this._internal.currentQuery.sort !== undefined) {
// We need to add it at the right index
for (var i = 0; i < _this._internal.collection.size(); i++)
if (QueryUtils.compareObjects(_this._internal.currentQuery.sort, _this._internal.collection.get(i), m) > 0)
break;
_this._internal.collection.addAtIndex(m, i);
} else {
_this._internal.collection.add(m);
}
// Make sure we don't exceed limit
let size = _this._internal.collection.size();
if (_this._internal.currentQuery.limit !== undefined && size > _this._internal.currentQuery.limit)
_this._internal.collection.remove(
_this._internal.collection.get(
_this._internal.currentQuery.sort !== undefined && _this._internal.currentQuery.sort[0][0] === '-'
? size - 1
: 0
)
);
//Send the array again over the items output to trigger function nodes etc that might be connected
_this.flagOutputDirty('items');
_this.flagOutputDirty('count');
_this.flagOutputDirty('firstItemId');
}
if (args.type === 'create') {
const m = Model.get(args.object.objectId);
if (m !== undefined) {
// Check if the object matches the current query
if (QueryUtils.matchesQuery(m, _this._internal.currentQuery.where)) {
// If matches the query, add the item to results
_addModelAtCorrectIndex(m);
}
}
} else if (args.type === 'save') {
const m = Model.get(args.objectId);
if (m !== undefined) {
const matchesQuery = QueryUtils.matchesQuery(m, _this._internal.currentQuery.where);
if (!matchesQuery && _this._internal.collection.contains(m)) {
// The record no longer matches the filter, remove it
_this._internal.collection.remove(m);
//Send the array again over the items output to trigger function nodes etc that might be connected
_this.flagOutputDirty('items');
_this.flagOutputDirty('count');
_this.flagOutputDirty('firstItemId');
} else if (matchesQuery && !_this._internal.collection.contains(m)) {
// It's not part of the result collection but now matches they query, add it and resort
_addModelAtCorrectIndex(m);
}
}
} else if (args.type === 'delete') {
const m = Model.get(args.objectId);
if (m !== undefined) {
_this._internal.collection.remove(m);
//Send the array again over the items output to trigger function nodes etc that might be connected
_this.flagOutputDirty('items');
_this.flagOutputDirty('count');
_this.flagOutputDirty('firstItemId');
}
}
};
// Listening to cloud store events is only for the global model scope, only valid in browser
// in cloud runtime its a nop
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
cloudstore.on('save', this._internal.cloudStoreEvents);
cloudstore.on('create', this._internal.cloudStoreEvents);
cloudstore.on('delete', this._internal.cloudStoreEvents);
this._internal.storageSettings = {};
},
getInspectInfo() {
const collection = this._internal.collection;
if (!collection) {
return { type: 'text', value: '[Not executed yet]' };
}
return [
{
type: 'value',
value: collection.items
}
];
},
inputs: {},
outputs: {
items: {
type: 'array',
displayName: 'Items',
group: 'General',
getter: function () {
return this._internal.collection;
}
},
firstItemId: {
type: 'string',
displayName: 'First Record Id',
group: 'General',
getter: function () {
if (this._internal.collection) {
var firstItem = this._internal.collection.get(0);
if (firstItem !== undefined) return firstItem.getId();
}
}
},
count: {
type: 'number',
displayName: 'Count',
group: 'General',
getter: function () {
return this._internal.collection ? this._internal.collection.size() : 0;
}
},
fetched: {
group: 'Events',
type: 'signal',
displayName: 'Success'
},
failure: {
group: 'Events',
type: 'signal',
displayName: 'Failure'
},
error: {
type: 'string',
displayName: 'Error',
group: 'Error',
getter: function () {
return this._internal.error;
}
}
},
prototypeExtensions: {
setCollectionName: function (name) {
this._internal.name = name;
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
},
setCollection: function (collection) {
this.bindCollection(collection);
this.flagOutputDirty('firstItemId');
this.flagOutputDirty('items');
this.flagOutputDirty('count');
},
unbindCurrentCollection: function () {
var collection = this._internal.collection;
if (!collection) return;
collection.off('change', this._internal.collectionChangedCallback);
this._internal.collection = undefined;
},
bindCollection: function (collection) {
this.unbindCurrentCollection();
this._internal.collection = collection;
collection && collection.on('change', this._internal.collectionChangedCallback);
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
this.unbindCurrentCollection();
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
cloudstore.off('insert', this._internal.cloudStoreEvents);
cloudstore.off('delete', this._internal.cloudStoreEvents);
cloudstore.off('save', this._internal.cloudStoreEvents);
},
setError: function (err) {
this._internal.err = err;
this.flagOutputDirty('error');
this.sendSignalOnOutput('failure');
},
scheduleFetch: function () {
var internal = this._internal;
if (internal.fetchScheduled) return;
internal.fetchScheduled = true;
this.scheduleAfterInputsHaveUpdated(() => {
internal.fetchScheduled = false;
this.fetch();
});
},
fetch: function () {
if (this.context.editorConnection) {
if (this._internal.name === undefined) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection', {
message: 'No collection specified for query'
});
} else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection');
}
}
const _c = Collection.get();
const f = this.getStorageFilter();
const limit = this.getStorageLimit();
const skip = this.getStorageSkip();
const count = this.getStorageFetchTotalCount();
this._internal.currentQuery = {
where: f.where,
sort: f.sort,
limit: limit,
skip: skip
};
CloudStore.forScope(this.nodeScope.modelScope).query({
collection: this._internal.name,
where: f.where,
sort: f.sort,
limit: limit,
skip: skip,
count: count,
success: (results,count) => {
if (results !== undefined) {
_c.set(
results.map((i) => {
var m = CloudStore._fromJSON(i, this._internal.name, this.nodeScope.modelScope);
return m;
})
);
}
if(count !== undefined) {
this._internal.storageSettings.storageTotalCount = count;
if(this.hasOutput('storageTotalCount'))
this.flagOutputDirty('storageTotalCount');
}
this.setCollection(_c);
this.sendSignalOnOutput('fetched');
},
error: (err) => {
this.setCollection(_c);
this.setError(err || 'Failed to fetch.');
}
});
},
getStorageFilter: function () {
const storageSettings = this._internal.storageSettings;
if (storageSettings['storageFilterType'] === undefined || storageSettings['storageFilterType'] === 'simple') {
// Create simple filter
const _where =
this._internal.visualFilter !== undefined
? QueryUtils.convertVisualFilter(this._internal.visualFilter, {
queryParameters: this._internal.queryParameters,
collectionName: this._internal.name
})
: undefined;
const _sort =
this._internal.visualSorting !== undefined
? QueryUtils.convertVisualSorting(this._internal.visualSorting)
: undefined;
return {
where: _where,
sort: _sort
};
} else if (storageSettings['storageFilterType'] === 'json') {
// JSON filter
if (!this._internal.filterFunc) {
try {
var filterCode = storageSettings['storageJSONFilter'];
// Parse out variables
filterCode = filterCode.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
this._internal.filterVariables = filterCode.match(/\$[A-Za-z0-9]+/g) || [];
var args = ['filter', 'where', 'sort', 'Inputs']
.concat(this._internal.filterVariables)
.concat([filterCode]);
this._internal.filterFunc = Function.apply(null, args);
} catch (e) {
this._internal.filterFunc = undefined;
console.log('Error while parsing filter script: ' + e);
}
}
if (!this._internal.filterFunc) return;
var _filter = {},
_sort = [],
_this = this;
// Collect filter variables
var _filterCb = function (f) {
_filter = QueryUtils.convertFilterOp(f, {
collectionName: _this._internal.name,
error: function (err) {
_this.context.editorConnection.sendWarning(
_this.nodeScope.componentOwner.name,
_this.id,
'query-collection-filter',
{
message: err
}
);
}
});
};
var _sortCb = function (s) {
_sort = s;
};
// Extract inputs
const inputs = {};
for (let key in storageSettings) {
if (key.startsWith('storageFilterValue-'))
inputs[key.substring('storageFilterValue-'.length)] = storageSettings[key];
}
var filterFuncArgs = [_filterCb, _filterCb, _sortCb, inputs]; // One for filter, one for where
this._internal.filterVariables.forEach((v) => {
filterFuncArgs.push(storageSettings['storageFilterValue-' + v.substring(1)]);
});
// Run the code to get the filter
try {
this._internal.filterFunc.apply(this, filterFuncArgs);
} catch (e) {
console.log('Error while running filter script: ' + e);
}
return { where: _filter, sort: _sort };
}
},
getStorageLimit: function () {
const storageSettings = this._internal.storageSettings;
if (!storageSettings['storageEnableLimit']) return;
else return storageSettings['storageLimit'] || 10;
},
getStorageSkip: function () {
const storageSettings = this._internal.storageSettings;
if (!storageSettings['storageEnableLimit']) return;
else return storageSettings['storageSkip'] || 0;
},
getStorageFetchTotalCount: function() {
const storageSettings = this._internal.storageSettings;
return !!storageSettings['storageEnableCount'];
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
this.registerOutput(name, {
getter: userOutputGetter.bind(this, name)
});
},
setVisualFilter: function (value) {
this._internal.visualFilter = value;
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
},
setVisualSorting: function (value) {
this._internal.visualSorting = value;
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
},
setQueryParameter: function (name, value) {
this._internal.queryParameters[name] = value;
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
},
registerInputIfNeeded: function (name) {
var _this = this;
if (this.hasInput(name)) {
return;
}
if (name.startsWith('qp-'))
return this.registerInput(name, {
set: this.setQueryParameter.bind(this, name.substring('qp-'.length))
});
const dynamicSignals = {
storageFetch: this.scheduleFetch.bind(this)
};
if (dynamicSignals[name])
return this.registerInput(name, {
set: EdgeTriggeredInput.createSetter({
valueChangedToTrue: dynamicSignals[name]
})
});
const dynamicSetters = {
collectionName: this.setCollectionName.bind(this),
visualFilter: this.setVisualFilter.bind(this),
visualSort: this.setVisualSorting.bind(this)
};
if (dynamicSetters[name])
return this.registerInput(name, {
set: dynamicSetters[name]
});
this.registerInput(name, {
set: userInputSetter.bind(this, name)
});
}
}
};
function userOutputGetter(name) {
/* jshint validthis:true */
return this._internal.storageSettings[name];
}
function userInputSetter(name, value) {
/* jshint validthis:true */
this._internal.storageSettings[name] = value;
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
}
const _defaultJSONQuery =
'// Write your query script here, check out the reference documentation for examples\n' + 'where({ })\n';
function updatePorts(nodeId, parameters, editorConnection, graphModel) {
var ports = [];
const dbCollections = graphModel.getMetaData('dbCollections');
const systemCollections = graphModel.getMetaData('systemCollections');
const _systemClasses = [
{ label: 'User', value: '_User' },
{ label: 'Role', value: '_Role' }
];
ports.push({
name: 'collectionName',
type: {
name: 'enum',
enums: _systemClasses.concat(
dbCollections !== undefined
? dbCollections.map((c) => {
return { value: c.name, label: c.name };
})
: []
),
allowEditOnly: true
},
displayName: 'Class',
plug: 'input',
group: 'General'
});
ports.push({
name: 'storageFilterType',
type: {
name: 'enum',
allowEditOnly: true,
enums: [
{ value: 'simple', label: 'Visual' },
{ value: 'json', label: 'Javascript' }
]
},
displayName: 'Filter',
default: 'simple',
plug: 'input',
group: 'General'
});
// Limit
ports.push({
type: 'boolean',
plug: 'input',
group: 'Limit',
name: 'storageEnableLimit',
displayName: 'Use limit'
});
if (parameters['storageEnableLimit']) {
ports.push({
type: 'number',
default: 10,
plug: 'input',
group: 'Limit',
name: 'storageLimit',
displayName: 'Limit'
});
ports.push({
type: 'number',
default: 0,
plug: 'input',
group: 'Limit',
name: 'storageSkip',
displayName: 'Skip'
});
}
ports.push({
type: 'signal',
plug: 'input',
group: 'Actions',
name: 'storageFetch',
displayName: 'Do'
});
// Total Count
ports.push({
type: 'boolean',
plug: 'input',
group: 'Total Count',
name: 'storageEnableCount',
displayName: 'Fetch total count'
});
if (parameters['storageEnableCount']) {
ports.push({
type: 'number',
plug: 'output',
group: 'General',
name: 'storageTotalCount',
displayName: 'Total Count'
});
}
// Simple query
if (parameters['storageFilterType'] === undefined || parameters['storageFilterType'] === 'simple') {
if (parameters.collectionName !== undefined) {
var c = dbCollections && dbCollections.find((c) => c.name === parameters.collectionName);
if (c === undefined && systemCollections) c = systemCollections.find((c) => c.name === parameters.collectionName);
if (c && c.schema && c.schema.properties) {
const schema = JSON.parse(JSON.stringify(c.schema));
// Find all records that have a relation with this type
function _findRelations(c) {
if (c.schema !== undefined && c.schema.properties !== undefined)
for (var key in c.schema.properties) {
var p = c.schema.properties[key];
if (p.type === 'Relation' && p.targetClass === parameters.collectionName) {
if (schema.relations === undefined) schema.relations = {};
if (schema.relations[c.name] === undefined) schema.relations[c.name] = [];
schema.relations[c.name].push({ property: key });
}
}
}
dbCollections && dbCollections.forEach(_findRelations);
systemCollections && systemCollections.forEach(_findRelations);
ports.push({
name: 'visualFilter',
plug: 'input',
type: { name: 'query-filter', schema: schema, allowEditOnly: true },
displayName: 'Filter',
group: 'Filter'
});
ports.push({
name: 'visualSort',
plug: 'input',
type: { name: 'query-sorting', schema: schema, allowEditOnly: true },
displayName: 'Sort',
group: 'Sorting'
});
}
if (parameters.visualFilter !== undefined) {
// Find all input ports
const uniqueInputs = {};
function _collectInputs(query) {
if (query === undefined) return;
if (query.rules !== undefined) query.rules.forEach((r) => _collectInputs(r));
else if (query.input !== undefined) uniqueInputs[query.input] = true;
}
_collectInputs(parameters.visualFilter);
Object.keys(uniqueInputs).forEach((input) => {
ports.push({
name: 'qp-' + input,
plug: 'input',
type: '*',
displayName: input,
group: 'Query Parameters'
});
});
}
}
}
// JSON query
else if (parameters['storageFilterType'] === 'json') {
ports.push({
type: { name: 'string', allowEditOnly: true, codeeditor: 'javascript' },
plug: 'input',
group: 'Filter',
name: 'storageJSONFilter',
default: _defaultJSONQuery,
displayName: 'Filter'
});
var filter = parameters['storageJSONFilter'];
if (filter) {
filter = filter.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
var variables = filter.match(/\$[A-Za-z0-9]+/g);
if (variables) {
const unique = {};
variables.forEach((v) => {
unique[v] = true;
});
Object.keys(unique).forEach((p) => {
ports.push({
name: 'storageFilterValue-' + p.substring(1),
displayName: p.substring(1),
group: 'Filter Values',
plug: 'input',
type: { name: '*', allowConnectionsOnly: true }
});
});
}
// Support variables with the "Inputs."" syntax
JavascriptNodeParser.parseAndAddPortsFromScript(filter, ports, {
inputPrefix: 'storageFilterValue-',
inputGroup: 'Filter Values',
inputType: { name: '*', allowConnectionsOnly: true },
skipOutputs: true
});
}
}
editorConnection.sendDynamicPorts(nodeId, ports);
}
module.exports = {
node: DbCollectionNode,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function _managePortsForNode(node) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
node.on('parameterUpdated', function (event) {
if (event.name.startsWith('storage') || event.name === 'visualFilter' || event.name === 'collectionName') {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
}
});
graphModel.on('metadataChanged.dbCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
});
graphModel.on('metadataChanged.systemCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
});
graphModel.on('metadataChanged.cloudservices', function (data) {
CloudStore.instance._initCloudServices();
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.DbCollection2', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('DbCollection2')) {
_managePortsForNode(node);
}
});
}
};

View File

@@ -0,0 +1,172 @@
'use strict';
const ConfigService = require('../../../api/configservice');
var ConfigNodeDefinition = {
name: 'DbConfig',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/config',
displayNodeName: 'Config',
category: 'Cloud Services',
usePortAsLabel: 'configKey',
color: 'data',
initialize: function () {
var internal = this._internal;
ConfigService.instance.getConfig().then((config) => {
internal.config = config;
if (this.hasOutput('value')) this.flagOutputDirty('value');
});
},
getInspectInfo() {
const value = this.getValue();
if (value === undefined) return '[No Value]';
return [{ type: 'value', value: value }];
},
inputs: {},
outputs: {},
methods: {
getValue: function () {
const internal = this._internal;
if (internal.useDevValue && this.context.editorConnection && this.context.editorConnection.isRunningLocally()) {
return internal.devValue;
} else if (internal.config !== undefined && internal.configKey !== undefined) {
return internal.config[internal.configKey];
}
},
setInternal: function (key, value) {
this._internal[key] = value;
if (this.hasOutput('value')) this.flagOutputDirty('value');
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
if (name === 'value')
return this.registerOutput(name, {
getter: this.getValue.bind(this)
});
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name === 'configKey' || name === 'useDevValue' || name === 'devValue')
return this.registerInput(name, {
set: this.setInternal.bind(this, name)
});
}
}
};
module.exports = {
node: ConfigNodeDefinition,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function updatePorts(node) {
var ports = [];
context.editorConnection.clearWarning(node.component.name, node.id, 'dbconfig-warning');
const configSchema = graphModel.getMetaData('dbConfigSchema');
let valueType;
if (configSchema) {
const isCloud = typeof _noodl_cloud_runtime_version !== 'undefined';
ports.push({
name: 'configKey',
displayName: 'Parameter',
group: 'General',
type: {
name: 'enum',
enums: Object.keys(configSchema)
.filter((k) => isCloud || !configSchema[k].masterKeyOnly)
.map((k) => ({ value: k, label: k })),
allowEditOnly: true
},
plug: 'input'
});
if (node.parameters['configKey'] !== undefined && configSchema && configSchema[node.parameters['configKey']]) {
valueType = configSchema[node.parameters['configKey']].type;
if (
valueType === 'string' ||
valueType === 'boolean' ||
valueType === 'number' ||
valueType === 'object' ||
valueType === 'array'
) {
ports.push({
name: 'useDevValue',
displayName: 'Enable',
group: 'Local Override',
type: 'boolean',
default: false,
plug: 'input'
});
if (node.parameters['useDevValue'] === true) {
ports.push({
name: 'devValue',
displayName: 'Value',
group: 'Local Override',
type: valueType,
plug: 'input'
});
}
}
} else if (node.parameters['configKey'] !== undefined) {
context.editorConnection.sendWarning(node.component.name, node.id, 'dbconfig-warning', {
showGlobally: true,
message: node.parameters['configKey'] + ' config parameter is missing, add it to your cloud service.'
});
}
} else {
context.editorConnection.sendWarning(node.component.name, node.id, 'dbconfig-warning', {
showGlobally: true,
message: 'You need an active cloud service.'
});
}
ports.push({
name: 'value',
displayName: 'Value',
group: 'General',
type: valueType || '*',
plug: 'output'
});
context.editorConnection.sendDynamicPorts(node.id, ports);
}
function _managePortsForNode(node) {
updatePorts(node);
node.on('parameterUpdated', function (event) {
updatePorts(node);
});
graphModel.on('metadataChanged.dbConfigSchema', function (data) {
ConfigService.instance.clearCache();
updatePorts(node);
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.DbConfig', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('DbConfig')) {
_managePortsForNode(node);
}
});
}
};

View File

@@ -0,0 +1,603 @@
'use strict';
const Model = require('../../../model');
const CloudStore = require('../../../api/cloudstore');
function _addBaseInfo(def, opts) {
const _includeInputProperties = opts === undefined || opts.includeInputProperties;
const _includeRelations = opts !== undefined && opts.includeRelations;
Object.assign(def.node, {
category: 'Data',
color: 'data',
inputs: def.node.inputs || {},
outputs: def.node.outputs || {},
methods: def.node.methods || {}
});
// Outputs
Object.assign(def.node.outputs, {
failure: {
type: 'signal',
displayName: 'Failure',
group: 'Events'
},
error: {
type: 'string',
displayName: 'Error',
group: 'Error',
getter: function () {
return this._internal.error;
}
}
});
// Methods
Object.assign(def.node.methods, {
scheduleOnce: function (type, cb) {
const _this = this;
const _type = 'hasScheduled' + type;
if (this._internal[_type]) return;
this._internal[_type] = true;
this.scheduleAfterInputsHaveUpdated(function () {
_this._internal[_type] = false;
cb();
});
},
checkWarningsBeforeCloudOp() {
//clear all errors first
this.clearWarnings();
if (!this._internal.collectionId) {
this.setError('No class name specified');
return false;
}
return true;
},
setError: function (err) {
this._internal.error = err;
this.flagOutputDirty('error');
this.sendSignalOnOutput('failure');
if (this.context.editorConnection) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
message: err,
showGlobally: true
});
}
},
clearWarnings() {
if (this.context.editorConnection) {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
}
}
});
// Setup
Object.assign(def, {
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function _managePortsForNode(node) {
function _updatePorts() {
var ports = [];
const dbCollections = graphModel.getMetaData('dbCollections');
const systemCollections = graphModel.getMetaData('systemCollections');
const _systemClasses = [
{ label: 'User', value: '_User' },
{ label: 'Role', value: '_Role' }
];
const parameters = node.parameters;
ports.push({
name: 'collectionName',
displayName: 'Class',
group: 'General',
type: {
name: 'enum',
enums: _systemClasses.concat(
dbCollections !== undefined
? dbCollections.map((c) => {
return { value: c.name, label: c.name };
})
: []
),
allowEditOnly: true
},
plug: 'input'
});
if (_includeRelations && parameters.collectionName && dbCollections) {
// Fetch ports from collection keys
var c = dbCollections.find((c) => c.name === parameters.collectionName);
if (c === undefined && systemCollections)
c = systemCollections.find((c) => c.name === parameters.collectionName);
if (c && c.schema && c.schema.properties) {
const props = c.schema.properties;
const enums = Object.keys(props)
.filter((key) => props[key].type === 'Relation')
.map((key) => ({ label: key, value: key }));
ports.push({
name: 'relationProperty',
displayName: 'Relation',
group: 'General',
type: { name: 'enum', enums: enums, allowEditOnly: true },
plug: 'input'
});
}
}
if (_includeInputProperties && parameters.collectionName && dbCollections) {
const _typeMap = {
String: 'string',
Boolean: 'boolean',
Number: 'number',
Date: 'date'
};
// Fetch ports from collection keys
var c = dbCollections.find((c) => c.name === parameters.collectionName);
if (c === undefined && systemCollections)
c = systemCollections.find((c) => c.name === parameters.collectionName);
if (c && c.schema && c.schema.properties) {
var props = c.schema.properties;
for (var key in props) {
var p = props[key];
if (ports.find((_p) => _p.name === key)) continue;
ports.push({
type: {
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
},
plug: 'input',
group: 'Properties',
name: 'prop-' + key,
displayName: key
});
}
}
}
def._additionalDynamicPorts && def._additionalDynamicPorts(node, ports, graphModel);
context.editorConnection.sendDynamicPorts(node.id, ports);
}
_updatePorts();
node.on('parameterUpdated', function (event) {
_updatePorts();
});
graphModel.on('metadataChanged.dbCollections', function (data) {
CloudStore.invalidateCollections();
_updatePorts();
});
graphModel.on('metadataChanged.systemCollections', function (data) {
CloudStore.invalidateCollections();
_updatePorts();
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.' + def.node.name, function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType(def.node.name)) {
_managePortsForNode(node);
}
});
}
});
}
function _addModelId(def, opts) {
var _def = { node: Object.assign({}, def.node), setup: def.setup };
var _methods = Object.assign({}, def.node.methods);
const _includeInputs = opts === undefined || opts.includeInputs;
const _includeOutputs = opts === undefined || opts.includeOutputs;
Object.assign(def.node, {
inputs: def.node.inputs || {},
outputs: def.node.outputs || {},
methods: def.node.methods || {}
});
if (_includeInputs) {
Object.assign(def.node, {
usePortAsLabel: 'collectionName'
});
def.node.dynamicports = (def.node.dynamicports || []).concat([
{
name: 'conditionalports/extended',
condition: 'idSource = explicit OR idSource NOT SET',
inputs: ['modelId']
}
]);
// Inputs
Object.assign(def.node.inputs, {
idSource: {
type: {
name: 'enum',
enums: [
{ label: 'Specify explicitly', value: 'explicit' },
{ label: 'From repeater', value: 'foreach' }
],
allowEditOnly: true
},
default: 'explicit',
displayName: 'Id Source',
group: 'General',
tooltip:
'Choose if you want to specify the Id explicitly, \n or if you want it to be that of the current record in a repeater.',
set: function (value) {
if (value === 'foreach') {
this.scheduleAfterInputsHaveUpdated(() => {
// Find closest nodescope that have a _forEachModel
var component = this.nodeScope.componentOwner;
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
component = component.parentNodeScope.componentOwner;
}
this.setModel(component !== undefined ? component._forEachModel : undefined);
});
}
}
},
modelId: {
type: {
name: 'string',
identifierOf: 'ModelName',
identifierDisplayName: 'Object Ids'
},
displayName: 'Id',
group: 'General',
set: function (value) {
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
this._internal.modelId = value; // Wait to fetch data
this.setModelID(value);
}
}
});
}
// Outputs
if (_includeOutputs) {
Object.assign(def.node.outputs, {
id: {
type: 'string',
displayName: 'Id',
group: 'General',
getter: function () {
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
}
}
});
}
// Methods
Object.assign(def.node.methods, {
setCollectionID: function (id) {
this._internal.collectionId = id;
this.clearWarnings();
},
setModelID: function (id) {
var model = (this.nodeScope.modelScope || Model).get(id);
this.setModel(model);
},
setModel: function (model) {
this._internal.model = model;
this.flagOutputDirty('id');
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name === 'collectionName')
this.registerInput(name, {
set: this.setCollectionID.bind(this)
});
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
}
});
}
function _addInputProperties(def) {
var _def = { node: Object.assign({}, def.node), setup: def.setup };
var _methods = Object.assign({}, def.node.methods);
Object.assign(def.node, {
inputs: def.node.inputs || {},
outputs: def.node.outputs || {},
methods: def.node.methods || {}
});
Object.assign(def.node, {
initialize: function () {
var internal = this._internal;
internal.inputValues = {};
_def.node.initialize && _def.node.initialize.call(this);
}
});
// Outputs
Object.assign(def.node.outputs, {});
// Inputs
Object.assign(def.node.inputs, {});
// Methods
Object.assign(def.node.methods, {
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name.startsWith('prop-'))
this.registerInput(name, {
set: this._setInputValue.bind(this, name.substring('prop-'.length))
});
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
},
_setInputValue: function (name, value) {
this._internal.inputValues[name] = value;
}
});
}
function _addRelationProperty(def) {
var _def = { node: Object.assign({}, def.node), setup: def.setup };
var _methods = Object.assign({}, def.node.methods);
Object.assign(def.node, {
inputs: def.node.inputs || {},
outputs: def.node.outputs || {},
methods: def.node.methods || {}
});
// Inputs
Object.assign(def.node.inputs, {
targetId: {
type: { name: 'string', allowConnectionsOnly: true },
displayName: 'Target Record Id',
group: 'General',
set: function (value) {
this._internal.targetModelId = value;
}
}
});
// Methods
Object.assign(def.node.methods, {
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name === 'relationProperty')
this.registerInput(name, {
set: this.setRelationProperty.bind(this)
});
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
},
setRelationProperty: function (value) {
this._internal.relationProperty = value;
}
});
}
function _getCurrentUser(modelScope) {
if (typeof _noodl_cloud_runtime_version === 'undefined') {
// We are running in browser, try to find the current user
var _cu = localStorage['Parse/' + CloudStore.instance.appId + '/currentUser'];
if (_cu !== undefined) {
let cu;
try {
cu = JSON.parse(_cu);
} catch (e) {}
return cu !== undefined ? cu.objectId : undefined;
}
} else {
// Assume we are running in cloud runtime
const request = modelScope.get('Request');
return request.UserId;
}
}
function _addAccessControl(def) {
var _def = { node: Object.assign({}, def.node), setup: def.setup };
var _methods = Object.assign({}, def.node.methods);
Object.assign(def.node, {
inputs: def.node.inputs || {},
outputs: def.node.outputs || {},
methods: def.node.methods || {}
});
Object.assign(def.node, {
initialize: function () {
var internal = this._internal;
internal.accessControl = {};
_def.node.initialize && _def.node.initialize.call(this);
}
});
// Inputs
Object.assign(def.node.inputs, {
accessControl: {
type: { name: 'proplist', autoName: 'Rule', allowEditOnly: true },
index: 1000,
displayName: 'Access Control Rules',
group: 'Access Control Rules',
set: function (value) {
this._internal.accessControlRules = value;
}
}
});
// Dynamic ports
const _super = def._additionalDynamicPorts;
def._additionalDynamicPorts = function (node, ports, graphModel) {
if (node.parameters['accessControl'] !== undefined && node.parameters['accessControl'].length > 0) {
node.parameters['accessControl'].forEach((ac) => {
const prefix = 'acl-' + ac.id;
// User or role?
ports.push({
name: prefix + '-target',
displayName: 'Target',
editorName: ac.label + ' | Target',
plug: 'input',
type: {
name: 'enum',
enums: [
{ value: 'user', label: 'User' },
{ value: 'everyone', label: 'Everyone' },
{ value: 'role', label: 'Role' }
],
allowEditOnly: true
},
group: ac.label + ' Access Rule',
default: 'user',
parent: 'accessControl',
parentItemId: ac.id
});
if (node.parameters[prefix + '-target'] === 'role') {
ports.push({
name: prefix + '-role',
displayName: 'Role',
editorName: ac.label + ' | Role',
group: ac.label + ' Access Rule',
plug: 'input',
type: 'string',
parent: 'accessControl',
parentItemId: ac.id
});
} else if (
node.parameters[prefix + '-target'] === undefined ||
node.parameters[prefix + '-target'] === 'user'
) {
ports.push({
name: prefix + '-userid',
displayName: 'User Id',
group: ac.label + ' Access Rule',
editorName: ac.label + ' | User Id',
plug: 'input',
type: { name: 'string', allowConnectionsOnly: true },
parent: 'accessControl',
parentItemId: ac.id
});
}
// Read
ports.push({
name: prefix + '-read',
displayName: 'Read',
editorName: ac.label + ' | Read',
group: ac.label + ' Access Rule',
plug: 'input',
type: { name: 'boolean' },
default: true,
parent: 'accessControl',
parentItemId: ac.id
});
// Write
ports.push({
name: prefix + '-write',
displayName: 'Write',
editorName: ac.label + ' | Write',
group: ac.label + ' Access Rule',
plug: 'input',
type: { name: 'boolean' },
default: true,
parent: 'accessControl',
parentItemId: ac.id
});
});
}
_super && _super(node, ports, graphModel);
};
// Methods
Object.assign(def.node.methods, {
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name.startsWith('acl-'))
this.registerInput(name, {
set: this.setAccessControl.bind(this, name)
});
_methods && _methods.registerInputIfNeeded && _methods.registerInputIfNeeded.call(this, name);
},
_getACL: function () {
let acl = {};
function _rule(rule) {
return {
read: rule.read === undefined ? true : rule.read,
write: rule.write === undefined ? true : rule.write
};
}
const currentUserId = _getCurrentUser(this.nodeScope.modelScope);
if (this._internal.accessControlRules !== undefined) {
this._internal.accessControlRules.forEach((r) => {
const rule = this._internal.accessControl[r.id];
if (rule === undefined) {
const userId = currentUserId;
if (userId !== undefined) acl[userId] = { write: true, read: true };
} else if (rule.target === 'everyone') {
acl['*'] = _rule(rule);
} else if (rule.target === 'user') {
const userId = rule.userid || currentUserId;
acl[userId] = _rule(rule);
} else if (rule.target === 'role') {
acl['role:' + rule.role] = _rule(rule);
}
});
}
return Object.keys(acl).length > 0 ? acl : undefined;
},
setAccessControl: function (name, value) {
const _parts = name.split('-');
if (this._internal.accessControl[_parts[1]] === undefined) this._internal.accessControl[_parts[1]] = {};
this._internal.accessControl[_parts[1]][_parts[2]] = value;
}
});
}
module.exports = {
addInputProperties: _addInputProperties,
addModelId: _addModelId,
addBaseInfo: _addBaseInfo,
addRelationProperty: _addRelationProperty,
addAccessControl: _addAccessControl
};

View File

@@ -0,0 +1,95 @@
'use strict';
var Model = require('../../../model');
var DbModelCRUDBase = require('./dbmodelcrudbase');
const CloudStore = require('../../../api/cloudstore');
var AddDbModelRelationNodeDefinition = {
node: {
name: 'AddDbModelRelation',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/add-record-relation',
displayNodeName: 'Add Record Relation',
usePortAsLabel: 'collectionName',
// shortDesc: "Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.",
inputs: {
store: {
displayName: 'Do',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleAddRelation();
}
}
},
outputs: {
relationAdded: {
type: 'signal',
displayName: 'Success',
group: 'Events'
}
},
methods: {
validateInputs: function () {
if (!this.context.editorConnection) return;
const _warning = (message) => {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation', {
message
});
};
if (this._internal.collectionId === undefined) {
_warning('No class specified');
} else if (this._internal.relationProperty === undefined) {
_warning('No relation property specified');
} else if (this._internal.targetModelId === undefined) {
_warning('No target record Id (the record to add a relation to) specified');
} else if (this._internal.model === undefined) {
_warning('No record Id specified (the record that should get the relation)');
} else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation');
}
},
scheduleAddRelation: function (key) {
const _this = this;
const internal = this._internal;
this.scheduleOnce('StorageAddRelation', function () {
_this.validateInputs();
if (!internal.model) return;
var model = internal.model;
var targetModelId = internal.targetModelId;
if (targetModelId === undefined) return;
CloudStore.forScope(_this.nodeScope.modelScope).addRelation({
collection: internal.collectionId,
objectId: model.getId(),
key: internal.relationProperty,
targetObjectId: targetModelId,
targetClass: (_this.nodeScope.modelScope || Model).get(targetModelId)._class,
success: function (response) {
for (var _key in response) {
model.set(_key, response[_key]);
}
// Successfully added relation
_this.sendSignalOnOutput('relationAdded');
},
error: function (err) {
_this.setError(err || 'Failed to add relation.');
}
});
});
}
}
}
};
DbModelCRUDBase.addBaseInfo(AddDbModelRelationNodeDefinition, {
includeRelations: true
});
DbModelCRUDBase.addModelId(AddDbModelRelationNodeDefinition);
DbModelCRUDBase.addRelationProperty(AddDbModelRelationNodeDefinition);
module.exports = AddDbModelRelationNodeDefinition;

View File

@@ -0,0 +1,94 @@
'use strict';
var Model = require('../../../model');
var DbModelCRUDBase = require('./dbmodelcrudbase');
const CloudStore = require('../../../api/cloudstore');
var AddDbModelRelationNodeDefinition = {
node: {
name: 'RemoveDbModelRelation',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/remove-record-relation',
displayName: 'Remove Record Relation',
usePortAsLabel: 'collectionName',
inputs: {
store: {
displayName: 'Do',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleRemoveRelation();
}
}
},
outputs: {
relationRemoved: {
type: 'signal',
displayName: 'Success',
group: 'Events'
}
},
methods: {
validateInputs: function () {
if (!this.context.editorConnection) return;
const _warning = (message) => {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation', {
message
});
};
if (this._internal.collectionId === undefined) {
_warning('No class specified');
} else if (this._internal.relationProperty === undefined) {
_warning('No relation property specified');
} else if (this._internal.targetModelId === undefined) {
_warning('No target record Id (the record to add a relation to) specified');
} else if (this._internal.model === undefined) {
_warning('No record Id specified (the record that should get the relation)');
} else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'add-relation');
}
},
scheduleRemoveRelation: function (key) {
const _this = this;
const internal = this._internal;
this.scheduleOnce('StorageRemoveRelation', function () {
_this.validateInputs();
if (!internal.model) return;
var model = internal.model;
var targetModelId = internal.targetModelId;
if (targetModelId === undefined) return;
CloudStore.forScope(_this.nodeScope.modelScope).removeRelation({
collection: internal.collectionId,
objectId: model.getId(),
key: internal.relationProperty,
targetObjectId: targetModelId,
targetClass: (_this.nodeScope.modelScope || Model).get(targetModelId)._class,
success: function (response) {
for (var _key in response) {
model.set(_key, response[_key]);
}
// Successfully removed relation
_this.sendSignalOnOutput('relationRemoved');
},
error: function (err) {
_this.setError(err || 'Failed to remove relation.');
}
});
});
}
}
}
};
DbModelCRUDBase.addBaseInfo(AddDbModelRelationNodeDefinition, {
includeRelations: true
});
DbModelCRUDBase.addModelId(AddDbModelRelationNodeDefinition);
DbModelCRUDBase.addRelationProperty(AddDbModelRelationNodeDefinition);
module.exports = AddDbModelRelationNodeDefinition;

View File

@@ -0,0 +1,401 @@
'use strict';
const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');
var Model = require('../../../model');
const CloudStore = require('../../../api/cloudstore');
var ModelNodeDefinition = {
name: 'DbModel2',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/record',
displayNodeName: 'Record',
shortDesc: 'Database model',
category: 'Cloud Services',
usePortAsLabel: 'collectionName',
color: 'data',
dynamicports: [
{
name: 'conditionalports/extended',
condition: 'idSource = explicit OR idSource NOT SET',
inputs: ['modelId']
}
],
initialize: function () {
var internal = this._internal;
internal.inputValues = {};
internal.relationModelIds = {};
var _this = this;
this._internal.onModelChangedCallback = function (args) {
if (_this.isInputConnected('fetch')) return;
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
_this.sendSignalOnOutput('changed');
};
},
getInspectInfo() {
const model = this._internal.model;
if (!model) return '[No Record]';
return [
{ type: 'text', value: 'Id: ' + model.getId() },
{ type: 'value', value: model.data }
];
},
outputs: {
id: {
type: 'string',
displayName: 'Id',
group: 'General',
getter: function () {
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
}
},
fetched: {
type: 'signal',
displayName: 'Fetched',
group: 'Events'
},
changed: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
},
failure: {
type: 'signal',
displayName: 'Failure',
group: 'Events'
},
error: {
type: 'string',
displayName: 'Error',
group: 'Error',
getter: function () {
return this._internal.error;
}
}
},
inputs: {
idSource: {
type: {
name: 'enum',
enums: [
{ label: 'Specify explicitly', value: 'explicit' },
{ label: 'From repeater', value: 'foreach' }
],
allowEditOnly: true
},
default: 'explicit',
displayName: 'Id Source',
group: 'General',
set: function (value) {
if (value === 'foreach') {
this.scheduleAfterInputsHaveUpdated(() => {
// Find closest nodescope that have a _forEachModel
var component = this.nodeScope.componentOwner;
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
component = component.parentNodeScope.componentOwner;
}
this.setModel(component !== undefined ? component._forEachModel : undefined);
});
}
}
},
modelId: {
type: { name: 'string', allowConnectionsOnly: true },
displayName: 'Id',
group: 'General',
set: function (value) {
if (value instanceof Model) value = value.getId();
// Can be passed as model as well
else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it
this._internal.modelId = value; // Wait to fetch data
if (this.isInputConnected('fetch') === false) this.setModelID(value);
else {
this.flagOutputDirty('id');
}
}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleFetch();
}
}
},
methods: {
setCollectionID: function (id) {
this._internal.collectionId = id;
},
setModelID: function (id) {
var model = (this.nodeScope.modelScope || Model).get(id);
// this._internal.modelIsNew = false;
this.setModel(model);
},
setModel: function (model) {
if (this._internal.model)
// Remove old listener if existing
this._internal.model.off('change', this._internal.onModelChangedCallback);
this._internal.model = model;
this.flagOutputDirty('id');
model.on('change', this._internal.onModelChangedCallback);
// We have a new model, mark all outputs as dirty
for (var key in model.data) {
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
}
this.sendSignalOnOutput('fetched');
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
},
scheduleOnce: function (type, cb) {
const _this = this;
const _type = 'hasScheduled' + type;
if (this._internal[_type]) return;
this._internal[_type] = true;
this.scheduleAfterInputsHaveUpdated(function () {
_this._internal[_type] = false;
cb();
});
},
setError: function (err) {
this._internal.error = err;
this.flagOutputDirty('error');
this.sendSignalOnOutput('failure');
if (this.context.editorConnection) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
message: err,
showGlobally: true
});
}
},
clearWarnings() {
if (this.context.editorConnection) {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
}
},
scheduleFetch: function () {
var _this = this;
const internal = this._internal;
this.scheduleOnce('Fetch', function () {
// Don't do fetch if no id
if (internal.modelId === undefined || internal.modelId === '') {
_this.setError('Missing Id.');
return;
}
const cloudstore = CloudStore.forScope(_this.nodeScope.modelScope);
cloudstore.fetch({
collection: internal.collectionId,
objectId: internal.modelId, // Get the objectId part of the model id
success: function (response) {
var model = cloudstore._fromJSON(response, internal.collectionId);
if (internal.model !== model) {
// Check if we need to change model
if (internal.model)
// Remove old listener if existing
internal.model.off('change', internal.onModelChangedCallback);
internal.model = model;
model.on('change', internal.onModelChangedCallback);
}
_this.flagOutputDirty('id');
delete response.objectId;
for (var key in response) {
if (_this.hasOutput('prop-' + key)) _this.flagOutputDirty('prop-' + key);
}
_this.sendSignalOnOutput('fetched');
},
error: function (err) {
_this.setError(err || 'Failed to fetch.');
}
});
});
},
scheduleStore: function () {
var _this = this;
var internal = this._internal;
if (!internal.model) return;
this.scheduleOnce('Store', function () {
for (var i in internal.inputValues) {
internal.model.set(i, internal.inputValues[i], { resolve: true });
}
});
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
if (name.startsWith('prop-'))
this.registerOutput(name, {
getter: userOutputGetter.bind(this, name.substring('prop-'.length))
});
},
registerInputIfNeeded: function (name) {
var _this = this;
if (this.hasInput(name)) {
return;
}
const dynamicSignals = {};
if (dynamicSignals[name])
return this.registerInput(name, {
set: EdgeTriggeredInput.createSetter({
valueChangedToTrue: dynamicSignals[name]
})
});
const dynamicSetters = {
collectionName: this.setCollectionID.bind(this)
};
if (dynamicSetters[name])
return this.registerInput(name, {
set: dynamicSetters[name]
});
if (name.startsWith('prop-'))
this.registerInput(name, {
set: userInputSetter.bind(this, name.substring('prop-'.length))
});
}
}
};
function userOutputGetter(name) {
/* jshint validthis:true */
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
}
function userInputSetter(name, value) {
//console.log('dbmodel setter:',name,value)
/* jshint validthis:true */
this._internal.inputValues[name] = value;
}
function updatePorts(nodeId, parameters, editorConnection, graphModel) {
var ports = [];
const dbCollections = graphModel.getMetaData('dbCollections');
const systemCollections = graphModel.getMetaData('systemCollections');
const _systemClasses = [
{ label: 'User', value: '_User' },
{ label: 'Role', value: '_Role' }
];
ports.push({
name: 'collectionName',
displayName: 'Class',
group: 'General',
type: {
name: 'enum',
enums: _systemClasses.concat(
dbCollections !== undefined
? dbCollections.map((c) => {
return { value: c.name, label: c.name };
})
: []
),
allowEditOnly: true
},
plug: 'input'
});
if (parameters.collectionName && dbCollections) {
// Fetch ports from collection keys
var c = dbCollections.find((c) => c.name === parameters.collectionName);
if (c === undefined && systemCollections) c = systemCollections.find((c) => c.name === parameters.collectionName);
if (c && c.schema && c.schema.properties) {
var props = c.schema.properties;
for (var key in props) {
var p = props[key];
if (ports.find((_p) => _p.name === key)) continue;
if (p.type === 'Relation') {
} else {
// Other schema type ports
const _typeMap = {
String: 'string',
Boolean: 'boolean',
Number: 'number',
Date: 'date'
};
ports.push({
type: {
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
},
plug: 'output',
group: 'Properties',
name: 'prop-' + key,
displayName: key
});
ports.push({
type: 'signal',
plug: 'output',
group: 'Changed Events',
displayName: key + ' Changed',
name: 'changed-' + key
});
}
}
}
}
editorConnection.sendDynamicPorts(nodeId, ports);
}
module.exports = {
node: ModelNodeDefinition,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function _managePortsForNode(node) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
node.on('parameterUpdated', function (event) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
});
graphModel.on('metadataChanged.dbCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
});
graphModel.on('metadataChanged.systemCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.DbModel2', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('DbModel2')) {
_managePortsForNode(node);
}
});
}
};

View File

@@ -0,0 +1,64 @@
'use strict';
var DbModelCRUDBase = require('./dbmodelcrudbase');
const CloudStore = require('../../../api/cloudstore');
var DeleteDbModelPropertiedNodeDefinition = {
node: {
name: 'DeleteDbModelProperties',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/delete-record',
displayNodeName: 'Delete Record',
shortDesc:
'Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.',
inputs: {
store: {
displayName: 'Do',
group: 'Actions',
valueChangedToTrue: function () {
this.storageDelete();
}
}
},
outputs: {
deleted: {
type: 'signal',
displayName: 'Success',
group: 'Events'
}
},
methods: {
storageDelete: function () {
const _this = this;
const internal = this._internal;
if (!this.checkWarningsBeforeCloudOp()) return;
this.scheduleOnce('StorageDelete', function () {
if (!internal.model) {
_this.setError('Missing Record Id');
return;
}
CloudStore.forScope(_this.nodeScope.ModelScope).delete({
collection: internal.collectionId,
objectId: internal.model.getId(), // Get the objectId part of the model id,
success: function () {
internal.model.notify('delete'); // Notify that this model has been deleted
_this.sendSignalOnOutput('deleted');
},
error: function (err) {
_this.setError(err || 'Failed to delete.');
}
});
});
}
}
}
};
DbModelCRUDBase.addBaseInfo(DeleteDbModelPropertiedNodeDefinition, {
includeInputProperties: false
});
DbModelCRUDBase.addModelId(DeleteDbModelPropertiedNodeDefinition);
module.exports = DeleteDbModelPropertiedNodeDefinition;

View File

@@ -0,0 +1,519 @@
'use strict';
const { Node } = require('../../../../noodl-runtime');
const Collection = require('../../../collection'),
Model = require('../../../model'),
CloudStore = require('../../../api/cloudstore'),
QueryUtils = require('../../../api/queryutils');
var FilterDBModelsNode = {
name: 'FilterDBModels',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/filter-records',
displayNodeName: 'Filter Records',
shortDesc: 'Filter, sort and limit array',
category: 'Data',
color: 'data',
initialize: function () {
var _this = this;
this._internal.collectionChangedCallback = function () {
if (_this.isInputConnected('filter') === true) return;
_this.scheduleFilter();
};
this._internal.cloudStoreEvents = function (args) {
if (_this.isInputConnected('filter') === true) return;
if (_this._internal.visualFilter === undefined) return;
if (_this._internal.collection === undefined) return;
if (args.collection !== _this._internal.collectionName) return;
if (args.objectId !== undefined && _this._internal.collection.contains(Model.get(args.objectId)))
_this.scheduleFilter();
};
CloudStore.instance.on('save', this._internal.cloudStoreEvents);
this._internal.enabled = true;
this._internal.filterSettings = {};
this._internal.filterParameters = {};
// this._internal.filteredCollection = Collection.get();
},
getInspectInfo() {
const collection = this._internal.filteredCollection;
if (!collection) {
return { type: 'text', value: '[Not executed yet]' };
}
return [
{
type: 'text',
value: 'Id: ' + collection.getId()
},
{
type: 'value',
value: collection.items
}
];
},
inputs: {
items: {
type: 'array',
displayName: 'Items',
group: 'General',
set(value) {
this.bindCollection(value);
if (this.isInputConnected('filter') === false) this.scheduleFilter();
}
},
enabled: {
type: 'boolean',
group: 'General',
displayName: 'Enabled',
default: true,
set: function (value) {
this._internal.enabled = value;
if (this.isInputConnected('filter') === false) this.scheduleFilter();
}
},
filter: {
type: 'signal',
group: 'Actions',
displayName: 'Filter',
valueChangedToTrue: function () {
this.scheduleFilter();
}
}
},
outputs: {
items: {
type: 'array',
displayName: 'Items',
group: 'General',
getter: function () {
return this._internal.filteredCollection;
}
},
firstItemId: {
type: 'string',
displayName: 'First Record Id',
group: 'General',
getter: function () {
if (this._internal.filteredCollection !== undefined) {
const firstItem = this._internal.filteredCollection.get(0);
if (firstItem !== undefined) return firstItem.getId();
}
}
},
/* firstItem:{
type: 'object',
displayName: 'First Item',
group: 'General',
getter: function () {
if(this._internal.filteredCollection !== undefined) {
return this._internal.filteredCollection.get(0);
}
}
}, */
count: {
type: 'number',
displayName: 'Count',
group: 'General',
getter: function () {
return this._internal.filteredCollection ? this._internal.filteredCollection.size() : 0;
}
},
modified: {
group: 'Events',
type: 'signal',
displayName: 'Filtered'
}
},
prototypeExtensions: {
unbindCurrentCollection: function () {
var collection = this._internal.collection;
if (!collection) return;
collection.off('change', this._internal.collectionChangedCallback);
this._internal.collection = undefined;
},
bindCollection: function (collection) {
this.unbindCurrentCollection();
this._internal.collection = collection;
collection && collection.on('change', this._internal.collectionChangedCallback);
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
this.unbindCurrentCollection();
CloudStore.instance.off('save', this._internal.cloudStoreEvents);
},
/* getFilter: function () {
const filterSettings = this._internal.filterSettings;
const options = ['case'] // List all supported options here
if (filterSettings['filterFilter']) {
const filters = filterSettings['filterFilter'].split(',');
var _filter = {};
filters.forEach(function (f) {
var op = '$' + (filterSettings['filterFilterOp-' + f] || 'eq');
_filter[f] = {};
_filter[f][op] = filterSettings['filterFilterValue-' + f];
options.forEach((o) => {
var option = filterSettings['filterFilterOption-' + o + '-' + f];
if(option) _filter[f]['$' + o] = option
})
})
return _filter;
}
},
getSort: function() {
const filterSettings = this._internal.filterSettings;
if (filterSettings['filterSort']) {
const sort = filterSettings['filterSort'].split(',');
var _sort = {};
sort.forEach(function (s) {
_sort[s] = filterSettings['filterSort-'+s] === 'descending'?-1:1;
})
return _sort;
}
},*/
getLimit: function () {
const filterSettings = this._internal.filterSettings;
if (!filterSettings['filterEnableLimit']) return;
else return filterSettings['filterLimit'] || 10;
},
getSkip: function () {
const filterSettings = this._internal.filterSettings;
if (!filterSettings['filterEnableLimit']) return;
else return filterSettings['filterSkip'] || 0;
},
scheduleFilter: function () {
if (this.collectionChangedScheduled) return;
this.collectionChangedScheduled = true;
this.scheduleAfterInputsHaveUpdated(() => {
this.collectionChangedScheduled = false;
if (!this._internal.collection) return;
// Apply filter and write to output collection
var filtered = [].concat(this._internal.collection.items);
if (this._internal.enabled) {
const _filter = this._internal.visualFilter;
if (_filter !== undefined) {
var filter = QueryUtils.convertVisualFilter(_filter, {
queryParameters: this._internal.filterParameters,
collectionName: this._internal.collectionName
});
if (filter) filtered = filtered.filter((m) => QueryUtils.matchesQuery(m, filter));
}
var _sort = this._internal.visualSorting;
if (_sort !== undefined && _sort.length > 0) {
var sort = QueryUtils.convertVisualSorting(_sort);
}
if (sort) filtered.sort(QueryUtils.compareObjects.bind(this, sort));
var skip = this.getSkip();
if (skip) filtered = filtered.slice(skip, filtered.length);
var limit = this.getLimit();
if (limit) filtered = filtered.slice(0, limit);
}
this._internal.filteredCollection = Collection.create(filtered);
this.sendSignalOnOutput('modified');
this.flagOutputDirty('firstItemId');
this.flagOutputDirty('items');
this.flagOutputDirty('count');
});
},
setCollectionName: function (name) {
this._internal.collectionName = name;
},
setVisualFilter: function (value) {
this._internal.visualFilter = value;
if (this.isInputConnected('filter') === false) this.scheduleFilter();
},
setVisualSorting: function (value) {
this._internal.visualSorting = value;
if (this.isInputConnected('filter') === false) this.scheduleFilter();
},
setFilterParameter: function (name, value) {
this._internal.filterParameters[name] = value;
if (this.isInputConnected('filter') === false) this.scheduleFilter();
},
registerInputIfNeeded: function (name) {
var _this = this;
if (this.hasInput(name)) {
return;
}
if (name === 'collectionName')
return this.registerInput(name, {
set: this.setCollectionName.bind(this)
});
if (name === 'visualFilter')
return this.registerInput(name, {
set: this.setVisualFilter.bind(this)
});
if (name === 'visualSorting')
return this.registerInput(name, {
set: this.setVisualSorting.bind(this)
});
if (name.startsWith('fp-'))
return this.registerInput(name, {
set: this.setFilterParameter.bind(this, name.substring('fp-'.length))
});
this.registerInput(name, {
set: userInputSetter.bind(this, name)
});
}
}
};
function userInputSetter(name, value) {
/* jshint validthis:true */
this._internal.filterSettings[name] = value;
if (this.isInputConnected('filter') === false) this.scheduleFilter();
}
function updatePorts(nodeId, parameters, editorConnection, dbCollections) {
var ports = [];
ports.push({
name: 'collectionName',
type: {
name: 'enum',
enums:
dbCollections !== undefined
? dbCollections.map((c) => {
return { value: c.name, label: c.name };
})
: [],
allowEditOnly: true
},
displayName: 'Class',
plug: 'input',
group: 'General'
});
ports.push({
type: 'boolean',
plug: 'input',
group: 'Limit',
name: 'filterEnableLimit',
displayName: 'Use limit'
});
if (parameters['filterEnableLimit']) {
ports.push({
type: 'number',
default: 10,
plug: 'input',
group: 'Limit',
name: 'filterLimit',
displayName: 'Limit'
});
ports.push({
type: 'number',
default: 0,
plug: 'input',
group: 'Limit',
name: 'filterSkip',
displayName: 'Skip'
});
}
if (parameters.collectionName !== undefined) {
var c = dbCollections.find((c) => c.name === parameters.collectionName);
if (c && c.schema && c.schema.properties) {
const schema = JSON.parse(JSON.stringify(c.schema));
const _supportedTypes = {
Boolean: true,
String: true,
Date: true,
Number: true,
Pointer: true
};
for (var key in schema.properties) {
if (!_supportedTypes[schema.properties[key].type]) delete schema.properties[key];
}
ports.push({
name: 'visualFilter',
plug: 'input',
type: { name: 'query-filter', schema: schema, allowEditOnly: true },
displayName: 'Filter',
group: 'Filter'
});
ports.push({
name: 'visualSorting',
plug: 'input',
type: { name: 'query-sorting', schema: schema, allowEditOnly: true },
displayName: 'Sorting',
group: 'Sorting'
});
}
if (parameters.visualFilter !== undefined) {
// Find all input ports
const uniqueInputs = {};
function _collectInputs(query) {
if (query === undefined) return;
if (query.rules !== undefined) query.rules.forEach((r) => _collectInputs(r));
else if (query.input !== undefined) uniqueInputs[query.input] = true;
}
_collectInputs(parameters.visualFilter);
Object.keys(uniqueInputs).forEach((input) => {
ports.push({
name: 'fp-' + input,
plug: 'input',
type: '*',
displayName: input,
group: 'Filter Parameters'
});
});
}
}
/* ports.push({
type: { name: 'stringlist', allowEditOnly: true },
plug: 'input',
group: 'Filter',
name: 'filterFilter',
displayName: 'Filter',
})
ports.push({
type: { name: 'stringlist', allowEditOnly: true },
plug: 'input',
group: 'Sort',
name: 'filterSort',
displayName: 'Sort',
})
const filterOps = {
"string": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' },{value: 'regex', label: 'Matches RegEx'}],
"boolean": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' }],
"number": [{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not Equals' }, { value: 'lt', label: 'Less than' }, { value: 'gt', label: 'Greater than' },
{ value: 'gte', label: 'Greater than or equal' }, { value: 'lte', label: 'Less than or equal' }]
}
if (parameters['filterFilter']) {
var filters = parameters['filterFilter'].split(',');
filters.forEach((f) => {
// Type
ports.push({
type: { name: 'enum', enums: [{ value: 'string', label: 'String' }, { value: 'number', label: 'Number' }, { value: 'boolean', label: 'Boolean' }] },
default: 'string',
plug: 'input',
group: f + ' filter',
displayName: 'Type',
editorName: f + ' filter | Type',
name: 'filterFilterType-' + f
})
var type = parameters['filterFilterType-' + f];
// String filter type
ports.push({
type: { name: 'enum', enums: filterOps[type || 'string'] },
default: 'eq',
plug: 'input',
group: f + ' filter',
displayName: 'Op',
editorName: f + ' filter| Op',
name: 'filterFilterOp-' + f
})
// Case sensitivite option
if(parameters['filterFilterOp-' + f] === 'regex') {
ports.push({
type: 'boolean',
default: false,
plug: 'input',
group: f + ' filter',
displayName: 'Case sensitive',
editorName: f + ' filter| Case',
name: 'filterFilterOption-case-' + f
})
}
ports.push({
type: type || 'string',
plug: 'input',
group: f + ' filter',
displayName: 'Value',
editorName: f + ' Filter Value',
name: 'filterFilterValue-' + f
})
})
}
if (parameters['filterSort']) {
var filters = parameters['filterSort'].split(',');
filters.forEach((f) => {
ports.push({
type: { name: 'enum', enums: [{ value: 'ascending', label: 'Ascending' }, { value: 'descending', label: 'Descending' }] },
default: 'ascending',
plug: 'input',
group: f + ' sort',
displayName: 'Sort',
editorName: f + ' sorting',
name: 'filterSort-' + f
})
})
}*/
editorConnection.sendDynamicPorts(nodeId, ports);
}
module.exports = {
node: FilterDBModelsNode,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
graphModel.on('nodeAdded.FilterDBModels', function (node) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
node.on('parameterUpdated', function (event) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
});
graphModel.on('metadataChanged.dbCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, data);
});
graphModel.on('metadataChanged.systemCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, data);
});
});
}
};

View File

@@ -0,0 +1,342 @@
'use strict';
const Collection = require('../../../collection');
const Model = require('../../../model');
function _addBaseInfo(def) {
Object.assign(def.node, {
category: 'Data',
color: 'data'
});
}
function _addModelId(def, opts) {
const _includeInputs = opts === undefined || opts.includeInputs;
const _includeOutputs = opts === undefined || opts.includeOutputs;
Object.assign(def.node, {
inputs: def.node.inputs || {},
outputs: def.node.outputs || {},
methods: def.node.methods || {}
});
if (_includeInputs) {
Object.assign(def.node, {
usePortAsLabel: 'modelId'
});
def.node.dynamicports = (def.node.dynamicports || []).concat([
{
name: 'conditionalports/extended',
condition: 'idSource = explicit OR idSource NOT SET',
inputs: ['modelId']
}
]);
// Inputs
Object.assign(def.node.inputs, {
idSource: {
type: {
name: 'enum',
enums: [
{ label: 'Specify explicitly', value: 'explicit' },
{ label: 'From repeater', value: 'foreach' }
],
allowEditOnly: true
},
default: 'explicit',
displayName: 'Id Source',
group: 'General',
set: function (value) {
if (value === 'foreach') {
this.scheduleAfterInputsHaveUpdated(() => {
// Find closest nodescope that have a _forEachModel
var component = this.nodeScope.componentOwner;
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
component = component.parentNodeScope.componentOwner;
}
this.setModel(component !== undefined ? component._forEachModel : undefined);
});
}
}
},
modelId: {
type: {
name: 'string',
identifierOf: 'ModelName',
identifierDisplayName: 'Object Ids'
},
displayName: 'Id',
group: 'General',
set: function (value) {
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
this._internal.modelId = value; // Wait to fetch data
this.setModelID(value);
}
}
});
}
// Outputs
if (_includeOutputs) {
Object.assign(def.node.outputs, {
id: {
type: 'string',
displayName: 'Id',
group: 'General',
getter: function () {
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
}
}
});
}
// Methods
Object.assign(def.node.methods, {
setModelID: function (id) {
var model = (this.nodeScope.modelScope || Model).get(id);
this.setModel(model);
},
setModel: function (model) {
this._internal.model = model;
this.flagOutputDirty('id');
}
});
//Inspect model
if (!def.node.getInspectInfo) {
def.node.getInspectInfo = function () {
const model = this._internal.model;
if (!model) return '[No Object]';
return [
{ type: 'text', value: 'Id: ' + model.getId() },
{ type: 'value', value: model.data }
];
};
}
}
function _addInputProperties(def) {
var _def = { node: Object.assign({}, def.node), setup: def.setup };
var _methods = Object.assign({}, def.node.methods);
Object.assign(def.node, {
inputs: def.node.inputs || {},
outputs: def.node.outputs || {},
methods: def.node.methods || {}
});
Object.assign(def, {
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
graphModel.on('nodeAdded.' + def.node.name, function (node) {
function _updatePorts() {
var ports = [];
const _types = [
{ label: 'String', value: 'string' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'Number', value: 'number' },
{ label: 'Date', value: 'date' },
{ label: 'Array', value: 'array' },
{ label: 'Object', value: 'object' },
{ label: 'Any', value: '*' }
];
// Add value outputs
var properties = node.parameters.properties;
if (properties) {
properties = properties ? properties.split(',') : undefined;
for (var i in properties) {
var p = properties[i];
// Property input
ports.push({
type: {
name: node.parameters['type-' + p] === undefined ? '*' : node.parameters['type-' + p]
},
plug: 'input',
group: 'Property Values',
displayName: p,
// editorName:p,
name: 'prop-' + p
});
// Property type
ports.push({
type: {
name: 'enum',
enums: _types,
allowEditOnly: true
},
plug: 'input',
group: 'Property Types',
displayName: p,
default: '*',
name: 'type-' + p
});
}
}
context.editorConnection.sendDynamicPorts(node.id, ports, {
detectRenamed: {
plug: 'input'
}
});
}
_updatePorts();
node.on('parameterUpdated', function (event) {
_updatePorts();
});
});
_def.setup && _def.setup(context, graphModel);
}
});
// Initilize
Object.assign(def.node, {
initialize: function () {
var internal = this._internal;
internal.inputValues = {};
internal.inputTypes = {};
_def.node.initialize && _def.node.initialize.call(this);
}
});
// Outputs
Object.assign(def.node.outputs, {});
// Inputs
Object.assign(def.node.inputs, {
properties: {
type: { name: 'stringlist', allowEditOnly: true },
displayName: 'Properties',
group: 'Properties to set',
set: function (value) {}
}
});
// Methods
Object.assign(def.node.methods, {
_pushInputValues: function (model) {
var internal = this._internal;
const _defaultValueForType = {
boolean: false,
string: '',
number: 0,
date: new Date()
};
const _allKeys = {};
for (const key in internal.inputTypes) _allKeys[key] = true;
for (const key in internal.inputValues) _allKeys[key] = true;
const properties = this.model.parameters.properties || '';
const validProperties = properties.split(',');
const keysToSet = Object.keys(_allKeys).filter((key) => validProperties.indexOf(key) !== -1);
for (const i of keysToSet) {
var value = internal.inputValues[i];
if (value !== undefined) {
//Parse array types with string as javascript
if (internal.inputTypes[i] !== undefined && internal.inputTypes[i] === 'array' && typeof value === 'string') {
this.context.editorConnection.clearWarning(
this.nodeScope.componentOwner.name,
this.id,
'invalid-array-' + i
);
try {
value = eval(value); //this might be static data in the form of javascript
} catch (e) {
if (value.indexOf('[') !== -1 || value.indexOf('{') !== -1) {
this.context.editorConnection.sendWarning(
this.nodeScope.componentOwner.name,
this.id,
'invalid-array-' + i,
{
showGlobally: true,
message: 'Invalid array<br>' + e.toString()
}
);
value = [];
} else {
//backwards compability with how this node used to work
value = Collection.get(value);
}
}
}
// Resolve object from IDs
if (
internal.inputTypes[i] !== undefined &&
internal.inputTypes[i] === 'object' &&
typeof value === 'string'
) {
value = (this.nodeScope.modelScope || Model).get(value);
}
model.set(i, value, { resolve: true });
} else {
model.set(i, _defaultValueForType[internal.inputTypes[i]], {
resolve: true
});
}
}
},
scheduleStore: function () {
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledStore = false;
if (!internal.model) return;
this._pushInputValues(internal.model);
this.sendSignalOnOutput('stored');
});
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name.startsWith('prop-'))
this.registerInput(name, {
set: this._setInputValue.bind(this, name.substring('prop-'.length))
});
if (name.startsWith('type-'))
this.registerInput(name, {
set: this._setInputType.bind(this, name.substring('type-'.length))
});
_methods && _methods.registerInputIfNeeded && _def.node.methods.registerInputIfNeeded.call(this, name);
},
_setInputValue: function (name, value) {
this._internal.inputValues[name] = value;
},
_setInputType: function (name, value) {
this._internal.inputTypes[name] = value;
}
});
}
module.exports = {
addInputProperties: _addInputProperties,
addModelId: _addModelId,
addBaseInfo: _addBaseInfo
};

View File

@@ -0,0 +1,277 @@
'use strict';
const { Node } = require('../../../../noodl-runtime');
var Model = require('../../../model');
var ModelNodeDefinition = {
name: 'Model2',
docs: 'https://docs.noodl.net/nodes/data/object/object-node',
displayNodeName: 'Object',
shortDesc:
'Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.',
category: 'Data',
usePortAsLabel: 'modelId',
color: 'data',
dynamicports: [
{
name: 'conditionalports/extended',
condition: 'idSource = explicit OR idSource NOT SET',
inputs: ['modelId']
}
],
initialize: function () {
var internal = this._internal;
internal.inputValues = {};
internal.dirtyValues = {};
var _this = this;
this._internal.onModelChangedCallback = function (args) {
if (_this.isInputConnected('fetch') === true) return;
if (_this.hasOutput('prop-' + args.name)) _this.flagOutputDirty('prop-' + args.name);
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
_this.sendSignalOnOutput('changed');
};
},
getInspectInfo() {
const model = this._internal.model;
if (!model) return '[No Object]';
return [
{ type: 'text', value: 'Id: ' + model.getId() },
{ type: 'value', value: model.data }
];
},
outputs: {
id: {
type: 'string',
displayName: 'Id',
group: 'General',
getter: function () {
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
}
},
changed: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
},
fetched: {
type: 'signal',
displayName: 'Fetched',
group: 'Events'
}
},
inputs: {
idSource: {
type: {
name: 'enum',
enums: [
{ label: 'Specify explicitly', value: 'explicit' },
{ label: 'From repeater', value: 'foreach' }
],
allowEditOnly: true
},
default: 'explicit',
displayName: 'Get Id from',
group: 'General',
set: function (value) {
if (value === 'foreach') {
this.scheduleAfterInputsHaveUpdated(() => {
// Find closest nodescope that have a _forEachModel
var component = this.nodeScope.componentOwner;
while (component !== undefined && component._forEachModel === undefined && component.parentNodeScope) {
component = component.parentNodeScope.componentOwner;
}
this.setModel(component !== undefined ? component._forEachModel : undefined);
});
}
}
},
modelId: {
type: {
name: 'string',
identifierOf: 'ModelName',
identifierDisplayName: 'Object Ids'
},
displayName: 'Id',
group: 'General',
set: function (value) {
if (value instanceof Model) value = value.getId();
// Can be passed as model as well
else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it
this._internal.modelId = value; // Wait to fetch data
if (this.isInputConnected('fetch') === false) this.setModelID(value);
else {
this.flagOutputDirty('id');
}
}
},
properties: {
type: { name: 'stringlist', allowEditOnly: true },
displayName: 'Properties',
group: 'Properties',
set: function (value) {}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleSetModel();
}
}
},
prototypeExtensions: {
scheduleStore: function () {
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledStore = false;
if (!internal.model) return;
for (var i in internal.dirtyValues) {
internal.model.set(i, internal.inputValues[i], { resolve: true });
}
internal.dirtyValues = {}; // Reset dirty values
});
},
scheduleSetModel: function () {
if (this.hasScheduledSetModel) return;
this.hasScheduledSetModel = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledSetModel = false;
this.setModelID(this._internal.modelId);
});
},
setModelID: function (id) {
var model = (this.nodeScope.modelScope || Model).get(id);
this.setModel(model);
this.sendSignalOnOutput('fetched');
},
setModel: function (model) {
if (this._internal.model)
// Remove old listener if existing
this._internal.model.off('change', this._internal.onModelChangedCallback);
this._internal.model = model;
this.flagOutputDirty('id');
// In set idSource, we are calling setModel with undefined
if (model) {
model.on('change', this._internal.onModelChangedCallback);
// We have a new model, mark all outputs as dirty
for (var key in model.data) {
if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key);
}
}
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
if (name.startsWith('prop-'))
this.registerOutput(name, {
getter: userOutputGetter.bind(this, name.substring('prop-'.length))
});
},
registerInputIfNeeded: function (name) {
var _this = this;
if (this.hasInput(name)) {
return;
}
if (name.startsWith('prop-'))
this.registerInput(name, {
set: userInputSetter.bind(this, name.substring('prop-'.length))
});
}
}
};
function userOutputGetter(name) {
/* jshint validthis:true */
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
}
function userInputSetter(name, value) {
/* jshint validthis:true */
this._internal.inputValues[name] = value;
// Store on change if no connection to store or new
const model = this._internal.model;
const valueChanged = model ? model.get(name) !== value : true;
if (valueChanged) {
this._internal.dirtyValues[name] = true;
this.scheduleStore();
}
}
function updatePorts(nodeId, parameters, editorConnection) {
var ports = [];
// Add value outputs
var properties = parameters.properties;
if (properties) {
properties = properties ? properties.split(',') : undefined;
for (var i in properties) {
var p = properties[i];
ports.push({
type: {
name: '*',
allowConnectionsOnly: true
},
plug: 'input/output',
group: 'Properties',
name: 'prop-' + p,
displayName: p
});
ports.push({
type: 'signal',
plug: 'output',
group: 'Changed Events',
displayName: p + ' Changed',
name: 'changed-' + p
});
}
}
editorConnection.sendDynamicPorts(nodeId, ports, {
detectRenamed: {
plug: 'input/output'
}
});
}
module.exports = {
node: ModelNodeDefinition,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
graphModel.on('nodeAdded.Model2', function (node) {
updatePorts(node.id, node.parameters, context.editorConnection);
node.on('parameterUpdated', function (event) {
updatePorts(node.id, node.parameters, context.editorConnection);
});
});
}
};

View File

@@ -0,0 +1,79 @@
'use strict';
var Model = require('../../../model');
var DbModelCRUDBase = require('./dbmodelcrudbase');
const CloudStore = require('../../../api/cloudstore');
var NewDbModelPropertiedNodeDefinition = {
node: {
name: 'NewDbModelProperties',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/create-new-record',
displayName: 'Create New Record',
usePortAsLabel: 'collectionName',
inputs: {
store: {
displayName: 'Do',
group: 'Actions',
valueChangedToTrue: function () {
this.storageInsert();
}
},
sourceObjectId: {
type: { name: 'string', allowConnectionsOnly: true },
displayName: 'Source Object Id',
group: 'General',
set: function (value) {
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
this._internal.sourceObjectId = value; // Wait to fetch data
}
}
},
outputs: {
created: {
type: 'signal',
displayName: 'Success',
group: 'Events'
}
},
methods: {
storageInsert: function () {
const internal = this._internal;
if (!this.checkWarningsBeforeCloudOp()) return;
this.scheduleOnce('StorageInsert', () => {
const initValues = Object.assign(
{},
internal.sourceObjectId ? (this.nodeScope.modelScope || Model).get(internal.sourceObjectId).data : {},
internal.inputValues
);
const cloudstore = CloudStore.forScope(this.nodeScope.modelScope);
cloudstore.create({
collection: internal.collectionId,
data: initValues,
acl: this._getACL(),
success: (data) => {
// Successfully created
const m = cloudstore._fromJSON(data, internal.collectionId);
this.setModel(m);
this.sendSignalOnOutput('created');
},
error: (err) => {
this.setError(err || 'Failed to insert.');
}
});
});
}
}
}
};
DbModelCRUDBase.addBaseInfo(NewDbModelPropertiedNodeDefinition);
DbModelCRUDBase.addModelId(NewDbModelPropertiedNodeDefinition, {
includeOutputs: true
});
DbModelCRUDBase.addInputProperties(NewDbModelPropertiedNodeDefinition);
DbModelCRUDBase.addAccessControl(NewDbModelPropertiedNodeDefinition);
module.exports = NewDbModelPropertiedNodeDefinition;

View File

@@ -0,0 +1,51 @@
'use strict';
var Model = require('../../../model');
var ModelCRUDBase = require('./modelcrudbase');
var NewModelNodeDefinition = {
node: {
name: 'NewModel',
docs: 'https://docs.noodl.net/nodes/data/object/create-new-object',
displayNodeName: 'Create New Object',
inputs: {
new: {
displayName: 'Do',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleNew();
}
}
},
outputs: {
created: {
type: 'signal',
displayName: 'Done',
group: 'Events'
}
},
methods: {
scheduleNew: function () {
if (this.hasScheduledNew) return;
this.hasScheduledNew = true;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledNew = false;
const newModel = (this.nodeScope.modelScope || Model).get();
this._pushInputValues(newModel);
this.setModel(newModel);
this.sendSignalOnOutput('created');
});
}
}
}
};
ModelCRUDBase.addBaseInfo(NewModelNodeDefinition);
ModelCRUDBase.addModelId(NewModelNodeDefinition, { includeOutputs: true });
ModelCRUDBase.addInputProperties(NewModelNodeDefinition);
module.exports = NewModelNodeDefinition;

View File

@@ -0,0 +1,587 @@
var defaultRequestScript =
'' +
'//Add custom code to setup the request object before the request\n' +
'//is made.\n' +
'//\n' +
'//*Request.resource contains the resource path of the request.\n' +
'//*Request.method contains the method, GET, POST, PUT or DELETE.\n' +
'//*Request.headers is a map where you can add additional headers.\n' +
'//*Request.parameters is a map the parameters that will be appended\n' +
'// to the url.\n' +
'//*Request.content contains the content of the request as a javascript\n' +
'// object.\n' +
'//\n';
('//*Inputs and *Outputs contain the inputs and outputs of the node.\n');
var defaultResponseScript =
'' +
'// Add custom code to convert the response content to outputs\n' +
'//\n' +
'//*Response.status The status code of the response\n' +
'//*Response.content The content of the response as a javascript\n' +
'// object.\n' +
'//*Response.request The request object that resulted in the response.\n' +
'//\n' +
'//*Inputs and *Outputs contain the inputs and outputs of the node.\n';
var RestNode = {
name: 'REST2',
displayNodeName: 'REST',
docs: 'https://docs.noodl.net/nodes/data/rest',
category: 'Data',
color: 'data',
searchTags: ['http', 'request', 'fetch'],
initialize: function () {
this._internal.inputValues = {};
this._internal.outputValues = {};
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
set: (obj, prop, value) => {
//only send outputs when they change.
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
if (value !== this._internal.outputValues[prop]) {
this.registerOutputIfNeeded('out-' + prop);
this._internal.outputValues[prop] = value;
this.flagOutputDirty('out-' + prop);
}
return true;
}
});
this._internal.self = {};
},
getInspectInfo() {
return this._internal.inspectData
? { type: 'value', value: this._internal.inspectData }
: { type: 'text', value: '[Not executed yet]' };
},
inputs: {
resource: {
type: 'string',
displayName: 'Resource',
group: 'Request',
default: '/',
set: function (value) {
this._internal.resource = value;
}
},
method: {
type: {
name: 'enum',
enums: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'PATCH', value: 'PATCH' },
{ label: 'DELETE', value: 'DELETE' }
]
},
displayName: 'Method',
group: 'Request',
default: 'GET',
set: function (value) {
this._internal.method = value;
}
},
/* scriptInputs: {
type: { name: 'proplist', allowEditOnly: true },
group: 'Inputs',
set: function (value) {
// this._internal.scriptInputs = value;
}
},
scriptOutputs: {
type: { name: 'proplist', allowEditOnly: true },
group: 'Outputs',
set: function (value) {
// this._internal.scriptOutputs = value;
}
},*/
requestScript: {
type: {
name: 'string',
allowEditOnly: true,
codeeditor: 'javascript'
},
displayName: 'Request',
default: defaultRequestScript,
group: 'Scripts',
set: function (script) {
try {
this._internal.requestFunc = new Function('Inputs', 'Outputs', 'Request', script);
} catch (e) {
console.log(e);
}
}
},
responseScript: {
type: {
name: 'string',
allowEditOnly: true,
codeeditor: 'javascript'
},
displayName: 'Response',
default: defaultResponseScript,
group: 'Scripts',
set: function (script) {
try {
this._internal.responseFunc = new Function('Inputs', 'Outputs', 'Response', script);
} catch (e) {
console.log(e);
}
}
},
fetch: {
type: 'signal',
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleFetch();
}
},
cancel: {
type: 'signal',
displayName: 'Cancel',
group: 'Actions',
valueChangedToTrue: function () {
this.cancelFetch();
}
}
},
outputs: {
failure: {
type: 'signal',
displayName: 'Failure',
group: 'Events'
},
success: {
type: 'signal',
displayName: 'Success',
group: 'Events'
},
canceled: {
type: 'signal',
displayName: 'Canceled',
group: 'Events'
}
},
prototypeExtensions: {
getScriptOutputValue: function (name) {
return this._internal.outputValues[name];
},
setScriptInputValue: function (name, value) {
return (this._internal.inputValues[name] = value);
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
if (name.startsWith('out-'))
return this.registerOutput(name, {
getter: this.getScriptOutputValue.bind(this, name.substring('out-'.length))
});
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name.startsWith('in-'))
return this.registerInput(name, {
set: this.setScriptInputValue.bind(this, name.substring('in-'.length))
});
/* if (name.startsWith('intype-')) return this.registerInput(name, {
set: function() {} // Ignore input type
})
if (name.startsWith('outtype-')) return this.registerInput(name, {
set: function() {} // Ignore output type
})*/
},
scheduleFetch: function () {
var internal = this._internal;
if (!internal.hasScheduledFetch) {
internal.hasScheduledFetch = true;
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
}
},
doResponse: function (status, response, request) {
// Process the response content with the response function
if (this._internal.responseFunc) {
this._internal.responseFunc.apply(this._internal.self, [
this._internal.inputValues,
this._internal.outputValuesProxy,
{ status: status, content: response, request: request }
]);
}
this._internal.inspectData = { status: status, content: response };
// Flag status
if (status >= 200 && status < 300) {
this.sendSignalOnOutput('success');
} else {
this.sendSignalOnOutput('failure');
}
},
doExternalFetch: function (request) {
var url = request.resource;
// Append parameters from request as query
if (Object.keys(request.parameters).length > 0) {
var parameters = Object.keys(request.parameters).map(function (p) {
return p + '=' + encodeURIComponent(request.parameters[p]);
});
url += '?' + parameters.join('&');
}
if (typeof _noodl_cloud_runtime_version === 'undefined') {
// Running in browser
var _this = this;
var xhr = new window.XMLHttpRequest();
this._xhr = xhr;
xhr.open(request.method, url, true);
for (var header in request.headers) {
xhr.setRequestHeader(header, request.headers[header]);
}
xhr.onreadystatechange = function () {
// XMLHttpRequest.DONE = 4, but torped runtime doesn't support enum
var sentResponse = false;
if (this.readyState === 4 || this.readyState === XMLHttpRequest.DONE) {
var statusCode = this.status;
var responseType = this.getResponseHeader('content-type');
var rawResponse = this.response;
delete this._xhr;
if (responseType) {
responseType = responseType.toLowerCase();
const responseData = responseType.indexOf('json') !== -1 ? JSON.parse(rawResponse) : rawResponse;
_this.doResponse(statusCode, responseData, request);
sentResponse = true;
}
if (sentResponse === false) {
_this.doResponse(statusCode, rawResponse, request);
}
}
};
xhr.onerror = function (e) {
//console.log('REST: Failed to request', url);
delete this._xhr;
_this.sendSignalOnOutput('failure');
};
xhr.onabort = function () {
delete this._xhr;
_this.sendSignalOnOutput('canceled');
};
if (request.content) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(request.content));
} else {
xhr.send();
}
} else {
// Running in cloud runtime
const headers = Object.assign(
{},
request.headers,
request.content
? {
'Content-Type': 'application/json'
}
: {}
);
fetch(url, {
method: request.method,
headers,
body: request.content ? JSON.stringify(request.content) : undefined
})
.then((response) => {
const responseType = response.headers.get('content-type');
if (responseType) {
if (responseType.indexOf('/json') !== -1) {
response.json().then((json) => {
this.doResponse(response.status, json, request);
});
} else {
if (this.context.editorConnection) {
this.context.editorConnection.sendWarning(
this.nodeScope.componentOwner.name,
this.id,
'rest-run-waring-',
{
message: 'REST only supports json content type in response.'
}
);
}
}
} else {
response.text().then((raw) => {
this.doResponse(response.status, raw, request);
});
}
})
.catch((e) => {
console.log('REST: Failed to request', url);
console.log(e);
this.sendSignalOnOutput('failure');
});
}
},
doFetch: function () {
this._internal.hasScheduledFetch = false;
// Format resource path
var resource = this._internal.resource;
if (resource) {
for (var key in this._internal.inputValues) {
resource = resource.replace('{' + key + '}', this._internal.inputValues[key]);
}
}
// Setup the request
var request = {
resource: resource,
headers: {},
method: this._internal.method !== undefined ? this._internal.method : 'GET',
parameters: {}
};
// Process the request content with the preprocess function
if (this._internal.requestFunc) {
this._internal.requestFunc.apply(this._internal.self, [
this._internal.inputValues,
this._internal.outputValuesProxy,
request
]);
}
// Perform request
this.doExternalFetch(request);
},
cancelFetch: function () {
if (typeof _noodl_cloud_runtime_version === 'undefined') {
this._xhr && this._xhr.abort();
} else {
if (this.context.editorConnection) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'rest-run-waring-', {
message: "REST doesn't support cancel in cloud functions."
});
}
}
}
}
};
function _parseScriptForErrors(script, args, name, node, context, ports) {
// Clear run warnings if the script is edited
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-run-waring-' + name);
if (script === undefined) {
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-parse-waring-' + name);
return;
}
try {
new Function(...args, script);
context.editorConnection.clearWarning(node.component.name, node.id, 'rest-parse-waring-' + name);
} catch (e) {
context.editorConnection.sendWarning(node.component.name, node.id, 'rest-parse-waring-' + name, {
message: '<strong>' + name + '</strong>: ' + e.message,
showGlobally: true
});
}
// Extract inputs and outputs
function _exists(port) {
return ports.find((p) => p.name === port) !== undefined;
}
const scriptWithoutComments = script.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
const inputs = scriptWithoutComments.match(/Inputs\.[A-Za-z0-9]+/g);
if (inputs) {
const unique = {};
inputs.forEach((v) => {
unique[v.substring('Inputs.'.length)] = true;
});
Object.keys(unique).forEach((p) => {
if (_exists('in-' + p)) return;
ports.push({
name: 'in-' + p,
displayName: p,
plug: 'input',
type: '*',
group: 'Inputs'
});
});
}
const outputs = scriptWithoutComments.match(/Outputs\.[A-Za-z0-9]+/g);
if (outputs) {
const unique = {};
outputs.forEach((v) => {
unique[v.substring('Outputs.'.length)] = true;
});
Object.keys(unique).forEach((p) => {
if (_exists('out-' + p)) return;
ports.push({
name: 'out-' + p,
displayName: p,
plug: 'output',
type: '*',
group: 'Outputs'
});
});
}
}
module.exports = {
node: RestNode,
setup: function (context, graphModel) {
if (!context.editorConnection) {
return;
}
function _managePortsForNode(node) {
function _updatePorts() {
if (!node.parameters) {
return;
}
var ports = [];
function exists(name) {
for (var i = 0; i < ports.length; i++) if (ports[i].name === name && ports[i].plug === 'input') return true;
return false;
}
/* const _typeEnums = [{value:'string',label:'String'},
{value:'boolean',label:'Boolean'},
{value:'number',label:'Number'},
{value:'color',label:'Color'},
{value:'object',label:'Object'},
{value:'array',label:'Array'}]*/
// Inputs
/* if (node.parameters['scriptInputs'] !== undefined && node.parameters['scriptInputs'].length > 0) {
node.parameters['scriptInputs'].forEach((p) => {
// Type for input
ports.push({
name: 'intype-' + p.label,
displayName: 'Type',
plug: 'input',
type: { name: 'enum', enums: _typeEnums, allowEditOnly: true },
default: 'string',
parent: 'scriptInputs',
parentItemId: p.id
})
// Default Value for input
ports.push({
name: 'in-' + p.label,
displayName: p.label,
plug: 'input',
type: node.parameters['intype-' + p.label] || 'string',
group: 'Input Values'
})
})
}*/
// Outputs
/* if (node.parameters['scriptOutputs'] !== undefined && node.parameters['scriptOutputs'].length > 0) {
node.parameters['scriptOutputs'].forEach((p) => {
// Type for output
ports.push({
name: 'outtype-' + p.label,
displayName: 'Type',
plug: 'input',
type: { name: 'enum', enums: _typeEnums, allowEditOnly: true },
default: 'string',
parent: 'scriptOutputs',
parentItemId: p.id
})
// Value for output
ports.push({
name: 'out-' + p.label,
displayName: p.label,
plug: 'output',
type: node.parameters['outtype-' + p.label] || '*',
group: 'Outputs',
})
})
}*/
// Parse resource path inputs
if (node.parameters.resource) {
var inputs = node.parameters.resource.match(/\{[A-Za-z0-9_]*\}/g);
for (var i in inputs) {
var def = inputs[i];
var name = def.replace('{', '').replace('}', '');
if (exists('in-' + name)) continue;
ports.push({
name: 'in-' + name,
displayName: name,
type: 'string',
plug: 'input',
group: 'Inputs'
});
}
}
if (node.parameters['requestScript']) {
_parseScriptForErrors(
node.parameters['requestScript'],
['Inputs', 'Outputs', 'Request'],
'Request script',
node,
context,
ports
);
}
if (node.parameters['responseScript']) {
_parseScriptForErrors(
node.parameters['responseScript'],
['Inputs', 'Outputs', 'Response'],
'Response script',
node,
context,
ports
);
}
context.editorConnection.sendDynamicPorts(node.id, ports);
}
_updatePorts();
node.on('parameterUpdated', function () {
_updatePorts();
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.REST2', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('REST2')) {
_managePortsForNode(node);
}
});
}
};

View File

@@ -0,0 +1,126 @@
'use strict';
var DbModelCRUDBase = require('./dbmodelcrudbase');
const CloudStore = require('../../../api/cloudstore');
var SetDbModelPropertiedNodeDefinition = {
node: {
name: 'SetDbModelProperties',
docs: 'https://docs.noodl.net/nodes/data/cloud-data/set-record-properties',
displayNodeName: 'Set Record Properties',
usePortAsLabel: 'collectionName',
dynamicports: [
{
name: 'conditionalports/extended',
condition: 'storeType = cloud OR storeType NOT SET',
inputs: ['storeProperties']
}
],
inputs: {
store: {
displayName: 'Do',
group: 'Actions',
valueChangedToTrue: function () {
if (this._internal.storeType === undefined || this._internal.storeType === 'cloud') this.scheduleSave();
else this.scheduleStore();
}
},
storeProperties: {
displayName: 'Properties to store',
group: 'General',
type: {
name: 'enum',
enums: [
{ label: 'Only specified', value: 'specified' },
{ label: 'All', value: 'all' }
]
},
default: 'specified',
set: function (value) {
this._internal.storeProperties = value;
}
},
storeType: {
displayName: 'Store to',
group: 'General',
type: {
name: 'enum',
enums: [
{ label: 'Cloud and local', value: 'cloud' },
{ label: 'Local only', value: 'local' }
]
},
default: 'cloud',
set: function (value) {
this._internal.storeType = value;
}
}
},
outputs: {
stored: {
type: 'signal',
displayName: 'Success',
group: 'Events'
}
},
methods: {
scheduleSave: function () {
const _this = this;
const internal = this._internal;
if (!this.checkWarningsBeforeCloudOp()) return;
this.scheduleOnce('StorageSave', function () {
if (!internal.model) {
_this.setError('Missing Record Id');
return;
}
var model = internal.model;
for (var i in internal.inputValues) {
model.set(i, internal.inputValues[i], { resolve: true });
}
CloudStore.forScope(_this.nodeScope.modelScope).save({
collection: internal.collectionId,
objectId: model.getId(), // Get the objectId part of the model id
data: internal.storeProperties === 'all' ? model.data : internal.inputValues, // Only store input values by default, if not explicitly specified
acl: _this._getACL(),
success: function (response) {
for (var key in response) {
model.set(key, response[key]);
}
_this.sendSignalOnOutput('stored');
},
error: function (err) {
_this.setError(err || 'Failed to save.');
}
});
});
},
scheduleStore: function () {
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledStore = false;
if (!internal.model) return;
for (var i in internal.inputValues) {
internal.model.set(i, internal.inputValues[i], { resolve: true });
}
this.sendSignalOnOutput('stored');
});
}
}
}
};
DbModelCRUDBase.addBaseInfo(SetDbModelPropertiedNodeDefinition);
DbModelCRUDBase.addModelId(SetDbModelPropertiedNodeDefinition);
DbModelCRUDBase.addInputProperties(SetDbModelPropertiedNodeDefinition);
DbModelCRUDBase.addAccessControl(SetDbModelPropertiedNodeDefinition);
module.exports = SetDbModelPropertiedNodeDefinition;

View File

@@ -0,0 +1,33 @@
'use strict';
var ModelCRUDBase = require('./modelcrudbase');
var SetModelPropertiedNodeDefinition = {
node: {
name: 'SetModelProperties',
docs: 'https://docs.noodl.net/nodes/data/object/set-object-properties',
displayNodeName: 'Set Object Properties',
inputs: {
store: {
displayName: 'Do',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleStore();
}
}
},
outputs: {
stored: {
type: 'signal',
displayName: 'Done',
group: 'Events'
}
}
}
};
ModelCRUDBase.addBaseInfo(SetModelPropertiedNodeDefinition);
ModelCRUDBase.addModelId(SetModelPropertiedNodeDefinition);
ModelCRUDBase.addInputProperties(SetModelPropertiedNodeDefinition);
module.exports = SetModelPropertiedNodeDefinition;