Files
fluxscape/packages/noodl-runtime/src/nodes/std-library/data/dbmodelnode2.js
2024-10-01 16:09:03 +02:00

409 lines
12 KiB
JavaScript

'use strict';
const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime');
const Model = require('../../../model');
const CloudStore = require('../../../api/cloudstore');
const 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 () {
const internal = this._internal;
internal.inputValues = {};
internal.relationModelIds = {};
const _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) {
// Can be passed as model as well
value = value.getId();
} else if (typeof value === 'object') {
// If this is an js object, dereference it
value = Model.create(value).getId();
}
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 () {
const _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) {
const 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 (const 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 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) {
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') {
// 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 () {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
});
graphModel.on('metadataChanged.dbCollections', function () {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, graphModel);
});
graphModel.on('metadataChanged.systemCollections', function () {
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);
}
});
}
};