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,4 @@
src
static
webpack-configs
tsconfig.json

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Future Platforms AB
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,13 @@
# Noodl Cloud runtime
The Noodl Cloud Runtime is a crucial component for executing Noodl Cloud functions.
This repository contains the code necessary to run and manage these functions seamlessly within the Noodl ecosystem.
## Usage
The Noodl Cloud Runtime is designed to be used in conjunction with the [Noodl Cloud Service](https://github.com/noodlapp/noodl-cloudservice).
Follow the instructions provided in the Noodl Cloud Service repository to set up the complete Noodl Cloud environment.
## License
This project is licensed under the MIT License.

View File

@@ -0,0 +1,22 @@
{
"name": "@noodl/cloud-runtime",
"author": "Noodl <info@noodl.net>",
"homepage": "https://noodl.net",
"version": "0.6.3",
"license": "MIT",
"main": "dist/main.js",
"scripts": {
"start": "webpack --config webpack-configs/webpack.dev.js",
"build": "webpack --config webpack-configs/webpack.prod.js",
"build:pack": "node ./scripts/pack.js"
},
"dependencies": {
"@noodl/runtime": "file:../noodl-runtime"
},
"devDependencies": {
"generate-json-webpack-plugin": "^2.0.0",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"copy-webpack-plugin": "^4.6.0"
}
}

View File

@@ -0,0 +1,32 @@
const fs = require('fs');
const path = require('path');
const publishDir = path.join(__dirname, '../publish');
const publishDistDir = path.join(__dirname, '../publish/dist/');
// Delete the publish folder if it exists
if (fs.existsSync(publishDir)) {
fs.rmdirSync(publishDir, { recursive: true, force: true });
}
// Create the publish folders
fs.mkdirSync(publishDistDir, { recursive: true });
// Copy over the wanted files
const files = ['dist/main.js', 'package.json', 'README.md', 'LICENSE'];
files.forEach((file) => {
const fromPath = path.join(__dirname, '..', file);
const toPath = path.join(publishDir, file);
fs.copyFileSync(fromPath, toPath);
});
// Clean up package.json
const packageFilePath = path.join(publishDir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageFilePath));
delete packageJson.scripts;
delete packageJson.dependencies;
delete packageJson.devDependencies;
fs.writeFileSync(packageFilePath, JSON.stringify(packageJson, null, 2));

View File

@@ -0,0 +1,19 @@
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
const files = {
async delete(fileName) {
return new Promise((resolve, reject) => {
CloudStore.instance.deleteFile({
file: { name: fileName },
success: (response) => {
resolve();
},
error: (e) => {
reject(e);
}
});
});
}
};
module.exports = files;

View File

@@ -0,0 +1,141 @@
const Model = require('@noodl/runtime/src/model');
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
const NoodlRuntime = require('@noodl/runtime');
const RecordsAPI = require('@noodl/runtime/src/api/records');
function createUsersAPI(modelScope) {
let _cloudstore;
const cloudstore = () => {
// We must create the cloud store just in time so all meta data is loaded
if (!_cloudstore) _cloudstore = new CloudStore(modelScope);
return _cloudstore;
};
const Records = RecordsAPI(modelScope);
const api = {
// This API should support an options object, this is how it works on
// the frontend and in the docs. To support any old use added a ugly fix
async logIn(username, password) {
if(typeof username === 'object' && password === undefined) {
const options = username;
username = options.username;
password = options.password;
}
return new Promise((resolve, reject) => {
const userService = NoodlRuntime.Services.UserService.forScope(modelScope);
userService.logIn({
username,
password,
success: (user) => {
resolve(user);
},
error: (e) => {
reject(Error(e));
}
});
});
},
async impersonate(username, options) {
// Look for the user based on username
const users = await Records.query('_User', {
username: { equalTo: username }
});
if (!users || users.length !== 1) {
throw Error('Could not find user.');
}
// See if there is a session already
const user = users[0];
const query = {
and: [{ user: { pointsTo: user.id } }, { expiresAt: { greaterThan: new Date() } }]
};
if (options && options.installationId) {
query.and.push({ installationId: { equalTo: options.installationId } });
}
const sessions = await Records.query('_Session', query);
async function _fetchUser(sessionToken) {
return new Promise((resolve, reject) => {
const userService = NoodlRuntime.Services.UserService.forScope(modelScope);
userService.fetchUser({
sessionToken,
success: (user) => resolve(user),
error: (e) => reject(Error(e))
});
});
}
if (!sessions || sessions.length === 0) {
// No session, we need to create one
const session = await Records.create('_Session', {
user: user.id,
installationId: options ? options.installationId : undefined,
sessionToken: 'r:' + Model.guid() + Model.guid(),
expiresAt: new Date(
Date.now() + (options && options.duration !== undefined ? options.duration : 24 * 60 * 60 * 1000)
),
restricted: false
});
return _fetchUser(session.sessionToken);
} else {
return _fetchUser(sessions[0].sessionToken);
}
}
};
Object.defineProperty(api, 'Current', {
get: function () {
const request = (modelScope || Model).get('Request');
const userId = request.get('UserId');
if (!userId) return;
return {
UserId: userId,
Properties: (modelScope || Model).get(userId),
async save(options) {
return new Promise((resolve, reject) => {
cloudstore().save({
collection: '_User',
objectId: userId,
data: (modelScope || Model).get(userId).data,
acl: options ? options.acl : undefined,
success: (response) => {
resolve();
},
error: (err) => {
reject(Error(err || 'Failed to save.'));
}
});
});
},
async fetch() {
return new Promise((resolve, reject) => {
cloudstore().fetch({
collection: '_User',
objectId: userId,
success: function (response) {
var record = cloudstore()._fromJSON(response, '_User');
resolve(record);
},
error: function (err) {
reject(Error(err || 'Failed to fetch.'));
}
});
});
}
};
}
});
return api;
}
module.exports = createUsersAPI;

View File

@@ -0,0 +1,16 @@
/**
* TODO: Define the different kind of inputs to the request node.
*/
export interface NoodlRequest {
headers: {};
body: string;
}
/**
* TODO: Define the different kind of outputs on the response node.
*/
export interface NoodlResponse {
statusCode: string;
headers: {};
body: string;
}

View File

@@ -0,0 +1,99 @@
// We need to import this so it's available in NoodlRuntime.Services
//import WebSocket from 'ws';
import { NoodlRequest, NoodlResponse } from './bridge';
import { registerNodes } from './nodes';
import NoodlRuntime from '@noodl/runtime';
import Model from '@noodl/runtime/src/model';
import NodeScope from '@noodl/runtime/src/nodescope';
import './noodl-js-api';
require('./services/userservice');
export class CloudRunner {
private runtime: NoodlRuntime;
constructor(options: {
webSocketClass?: any;
enableDebugInspectors?: boolean;
connectToEditor?: boolean;
editorAddress?: string;
}) {
this.runtime = new NoodlRuntime({
type: 'cloud',
platform: {
requestUpdate: (f: any) => setImmediate(f),
getCurrentTime: () => new Date().getTime(),
objectToString: (o: any) => JSON.stringify(o, null, 2),
webSocketClass: options.webSocketClass,
isRunningLocally: () => options.connectToEditor
},
componentFilter: (c) => c.name.startsWith('/#__cloud__/'),
dontCreateRootComponent: true
});
registerNodes(this.runtime);
this.runtime.setDebugInspectorsEnabled(options.enableDebugInspectors);
if (options.connectToEditor && options.editorAddress) {
this.runtime.connectToEditor(options.editorAddress);
}
}
public async load(exportData: any, projectSettings?: any) {
await this.runtime.setData(exportData);
if (projectSettings) this.runtime.setProjectSettings(projectSettings);
}
public async run(functionName: string, request: NoodlRequest): Promise<NoodlResponse> {
return new Promise<NoodlResponse>((resolve, reject) => {
const requestId = Math.random().toString(26).slice(2);
const requestScope = new NodeScope(this.runtime.context);
requestScope.modelScope = new Model.Scope();
this.runtime.context
.createComponentInstanceNode('/#__cloud__/' + functionName, requestId + '-' + functionName, requestScope)
.then((functionComponent) => {
// Look for the first request node (should only be one)
const requestNode = functionComponent.nodeScope.getNodesWithType('noodl.cloud.request')[0];
if (requestNode) {
// Look for all response nodes
let hasResponded = false;
const responseNodes = functionComponent.nodeScope.getNodesWithTypeRecursive('noodl.cloud.response');
responseNodes.forEach((resp) => {
resp._internal._sendResponseCallback = (resp) => {
if (hasResponded) return;
hasResponded = true;
//the functionComponent is "manually" created outside of a scope, so call the delete function directly
functionComponent._onNodeDeleted();
requestScope.reset(); //this deletes any remaining nodes, although there shouldn't be any at this point
//clean upp all models
requestScope.modelScope.reset();
resolve(resp);
};
});
setImmediate(() => {
try {
requestNode.sendRequest(request).catch(reject);
} catch (e) {
reject(e);
}
});
} else {
reject(Error('Could not find request node for function'));
}
})
.catch((e) => {
// Failed creating component
reject(e);
});
});
}
}

View File

@@ -0,0 +1,185 @@
//import CloudStore from '@noodl/runtime/src/api/cloudstore'
import Model from '@noodl/runtime/src/model';
import NoodlRuntime from '@noodl/runtime';
import ConfigService from '@noodl/runtime/src/api/configservice'
export const node = {
name: 'noodl.cloud.request',
displayNodeName: 'Request',
category: 'Cloud',
docs: 'https://docs.noodl.net/nodes/cloud-functions/request',
useVariants: false,
mountedInput: false,
allowAsExportRoot: false,
singleton: true,
color: 'data',
connectionPanel: {
groupPriority: ['General', 'Mounted']
},
outputs: {
receive: {
displayName: 'Received',
type: 'signal',
group: 'General'
},
auth: {
displayName: 'Authenticated',
type: 'boolean',
group: 'Request',
getter: function () {
return !!this._internal.authenticated;
}
},
userId: {
displayName: 'User Id',
type: 'boolean',
group: 'Request',
getter: function () {
return this._internal.authUserId;
}
}
},
inputs: {
allowNoAuth: {
group: 'General',
type: 'boolean',
displayName: 'Allow Unauthenticated',
default: false,
set: function (value) {
this._internal.allowNoAuth = value;
}
},
params: {
group: 'Parameters',
type: { name: 'stringlist', allowEditOnly: true },
set: function (value) {
this._internal.params = value;
}
}
},
initialize: function () {
this._internal.allowNoAuth = false;
this._internal.requestParameters = {};
this._internal.userProperties = {
Authenticated: false
};
},
methods: {
getRequestParameter: function (name) {
return this._internal.requestParameters[name];
},
setRequestParameter: function (name, value) {
this._internal.requestParameters[name] = value;
if (this.hasOutput('pm-' + name)) this.flagOutputDirty('pm-' + name);
},
fetchCurrentUser: async function (sessionToken) {
return new Promise((resolve, reject) => {
const userService = NoodlRuntime.Services.UserService.forScope(this.nodeScope.modelScope);
userService.fetchCurrentUser({
sessionToken,
success: resolve,
error: reject
});
});
},
sendRequest: async function (req) {
const sessionToken = req.headers['x-parse-session-token'];
let params = {};
try {
params = JSON.parse(req.body);
} catch (e) {}
if (sessionToken) {
// There is a user token, fetch user
try {
await this.fetchCurrentUser(sessionToken);
const userService = NoodlRuntime.Services.UserService.forScope(this.nodeScope.modelScope);
const userModel = userService.current;
this._internal.authenticated = true;
this._internal.authUserId = userModel.getId();
this.flagOutputDirty('userId');
} catch (e) {
// User could not be fetched
if (!this._internal.allowNoAuth) throw Error('Unauthenticated requests not accepted.');
}
} else if (!this._internal.allowNoAuth) throw Error('Unauthenticated requests not accepted.');
// Make sure config is cached before processing request
await ConfigService.instance.getConfig()
// Create request object
const requestModel = (this.nodeScope.modelScope || Model).get('Request');
requestModel.set('Authenticated', !!this._internal.authenticated);
requestModel.set('UserId', this._internal.authUserId);
requestModel.set('Parameters', params);
requestModel.set('Headers', req.headers);
this.flagOutputDirty('auth');
for (let key in params) {
this.setRequestParameter(key, params[key]);
}
this.sendSignalOnOutput('receive');
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
if (name.startsWith('pm-'))
this.registerOutput(name, {
getter: this.getRequestParameter.bind(this, name.substring('pm-'.length))
});
}
}
};
export function setup(context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function _managePortsForNode(node) {
function _updatePorts() {
var ports = [];
// Add params outputs
var params = node.parameters.params;
if (params !== undefined) {
params = params.split(',');
for (var i in params) {
var p = params[i];
ports.push({
type: '*',
plug: 'output',
group: 'Parameters',
name: 'pm-' + p,
displayName: p
});
}
}
context.editorConnection.sendDynamicPorts(node.id, ports);
}
_updatePorts();
node.on('parameterUpdated', function (event) {
if (event.name === 'params') {
_updatePorts();
}
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.noodl.cloud.request', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('noodl.cloud.request')) {
_managePortsForNode(node);
}
});
}

View File

@@ -0,0 +1,149 @@
export const node = {
name: 'noodl.cloud.response',
displayNodeName: 'Response',
category: 'Cloud',
docs: 'https://docs.noodl.net/nodes/cloud-functions/response',
useVariants: false,
mountedInput: false,
allowAsExportRoot: false,
color: "data",
connectionPanel: {
groupPriority: ['General', 'Mounted']
},
dynamicports:[
{
name:'conditionalports/extended',
condition:"status = success OR status NOT SET",
inputs:['params']
},
{
name:'conditionalports/extended',
condition:"status = failure",
inputs:['errorMessage']
}
],
initialize:function() {
this._internal.responseParameters = {}
},
inputs: {
params: {
group: 'Parameters',
type:{name:'stringlist',allowEditOnly:true},
set: function (value) {
this._internal.params = value;
}
},
errorMessage: {
group: 'General',
type: 'string',
displayName:'Error Message',
set: function (value) {
this._internal.errorMessage = value;
}
},
send: {
displayName: 'Send',
type: 'signal',
group: 'General',
valueChangedToTrue: function () {
if(this._internal.status === undefined || this._internal.status === 'success') {
this._internal._sendResponseCallback({
statusCode: 200,
body: JSON.stringify({result:this._internal.responseParameters})
})
}
else {
this._internal._sendResponseCallback({
statusCode: 400,
body: JSON.stringify({error:this._internal.errorMessage})
})
}
}
},
status: {
group: 'General',
displayName:'Status',
type: {
name: 'enum',
enums: [
{
label: 'Success',
value: 'success'
},
{
label: 'Failure',
value: 'failure'
}
]
},
default:'success',
set: function(value) {
this._internal.status = value;
}
}
},
methods:{
setResponseParameter:function(name,value) {
this._internal.responseParameters[name] = value
},
registerInputIfNeeded: function(name) {
if(this.hasInput(name)) {
return;
}
if(name.startsWith('pm-')) this.registerInput(name, {
set: this.setResponseParameter.bind(this, name.substring('pm-'.length))
})
},
}
};
export function setup(context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function _managePortsForNode(node) {
function _updatePorts() {
var ports = []
// Add params outputs
if(node.parameters.status === 'success' || node.parameters.status === undefined) {
var params = node.parameters.params;
if (params !== undefined) {
params = params.split(',');
for (var i in params) {
var p = params[i];
ports.push({
type: '*',
plug: 'input',
group: 'Parameters',
name: 'pm-' + p,
displayName: p
})
}
}
}
context.editorConnection.sendDynamicPorts(node.id, ports);
}
_updatePorts();
node.on("parameterUpdated", function (event) {
if (event.name === 'params') {
_updatePorts();
}
});
}
graphModel.on("editorImportComplete", () => {
graphModel.on("nodeAdded.noodl.cloud.response", function (node) {
_managePortsForNode(node)
})
for (const node of graphModel.getNodesWithType('noodl.cloud.response')) {
_managePortsForNode(node)
}
})
}

View File

@@ -0,0 +1,602 @@
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
const CloudStore = require('@noodl/runtime/src/api/cloudstore'),
JavascriptNodeParser = require('@noodl/runtime/src/javascriptnodeparser'),
QueryUtils = require('@noodl/runtime/src/api/queryutils');
var AggregateNode = {
name: 'noodl.cloud.aggregate',
docs: 'https://docs.noodl.net/nodes/cloud-functions/cloud-data/aggregate-records',
displayName: 'Aggregate Records',
category: 'Cloud Services',
usePortAsLabel: 'collectionName',
color: 'data',
initialize: function () {
this._internal.queryParameters = {};
this._internal.storageSettings = {};
this._internal.aggregates = {};
},
getInspectInfo() {
const aggregates = this._internal.aggregateValues;
if (!aggregates) {
return { type: 'text', value: '[Not executed yet]' };
}
return [
{
type: 'value',
value: aggregates
}
];
},
inputs: {
aggregates:{
index:100,
group:"Aggregates",
type:{name:"stringlist",allowEditOnly:true},
displayName:"Aggregates",
set:function(value) {
this._internal.aggregatesList = value;
}
}
},
outputs: {
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();
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
},
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) {
let validateAggNames = true;
if(this._internal.aggregatesList) {
this._internal.aggregatesList.split(',').forEach(k => {
if(k.indexOf(' ')!==-1)
validateAggNames = false;
})
}
if (this._internal.name === undefined) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'aggregate-node', {
message: 'No class specified for aggregate.'
});
}
else if(this._internal.aggregatesList === undefined) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'aggregate-node', {
message: 'No aggregates specified.'
});
}
else if(!validateAggNames) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'aggregate-node', {
message: 'Invalid aggregate names, dont use space and special characters.'
});
}
else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'aggregate-node');
}
}
const f = this.getStorageFilter();
this._internal.currentQuery = {
where: f.where,
};
CloudStore.forScope(this.nodeScope.modelScope).aggregate({
collection: this._internal.name,
where: f.where,
group: this.getAggregates(),
success: (results) => {
this._internal.aggregateValues = results;
for(const key in results) {
if(this.hasOutput('agg-'+key))
this.flagOutputDirty('agg-'+key);
}
this.sendSignalOnOutput('fetched');
},
error: (err) => {
this.setError(err || 'Failed to aggregate.');
}
});
},
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;
return {
where: _where,
};
} 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 = ['where', '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 = {},
_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,
'aggregate-node-filter',
{
message: err
}
);
}
});
};
// Extract inputs
const inputs = {};
for (let key in storageSettings) {
if (key.startsWith('storageFilterValue-'))
inputs[key.substring('storageFilterValue-'.length)] = storageSettings[key];
}
var filterFuncArgs = [ _filterCb, 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 };
}
},
getAggregates:function() {
if(!this._internal.aggregatesList) return {};
if(!this._internal.aggregates) return {};
const schema = CloudStore._collections;
const classSchema = schema[this._internal.name];
if(!classSchema || !classSchema.schema || !classSchema.schema.properties) return {};
const defs = this._internal.aggregates;
const aggs = {};
this._internal.aggregatesList.split(',').forEach(a => {
if(defs[a].prop === undefined) return;
const propSchema = classSchema.schema.properties[defs[a].prop];
if(propSchema === undefined) return;
const op = {}
const _def = (propSchema.type === 'String')?'distinct':'avg';
op[defs[a].op || _def] = defs[a].prop;
aggs[a] = op;
})
return aggs;
},
getAggregateValue:function(name) {
if(!this._internal.aggregateValues) return;
return this._internal.aggregateValues[name];
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
if(name.startsWith('agg-')) this.registerOutput(name, {
getter: this.getAggregateValue.bind(this, name.substring('agg-'.length))
});
},
setVisualFilter: function (value) {
this._internal.visualFilter = value;
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
},
setQueryParameter: function (name, value) {
this._internal.queryParameters[name] = value;
if (this.isInputConnected('storageFetch') === false) this.scheduleFetch();
},
setAggregateParameter:function(name,value) {
const aggregates = this._internal.aggregates;
if(name.startsWith('aggprop-')) {
const _name = name.substring('aggprop-'.length);
if(!aggregates[_name]) aggregates[_name] = {}
aggregates[_name].prop = value;
}
else if(name.startsWith('aggop-')) {
const _name = name.substring('aggop-'.length);
if(!aggregates[_name]) aggregates[_name] = {}
aggregates[_name].op = value;
}
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
if (name.startsWith('agg'))
return this.registerInput(name, {
set: this.setAggregateParameter.bind(this, name)
});
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)
};
if (dynamicSetters[name])
return this.registerInput(name, {
set: dynamicSetters[name]
});
this.registerInput(name, {
set: userInputSetter.bind(this, 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'
});
ports.push({
type: 'signal',
plug: 'input',
group: 'Actions',
name: 'storageFetch',
displayName: 'Do'
});
if(parameters['aggregates'] !== undefined && 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 aggs = parameters['aggregates'].split(',');
const props = Object.keys(c.schema.properties).filter(k => c.schema.properties[k].type === 'Number' || c.schema.properties[k].type === 'String');
aggs.forEach(a => {
ports.push({
index:101,
name:'aggprop-'+a,
plug:'input',
type:{name:'enum',enums:props.map(k => ({value:k,label:k})),allowEditOnly:true},
displayName:'Property',
group:a
});
if(parameters['aggprop-'+a] !== undefined) {
const prop = parameters['aggprop-'+a];
const schema = c.schema.properties[prop];
if(schema && schema.type === 'Number') {
// Number aggregate
ports.push({
index:102,
name: 'aggop-' + a,
plug: 'input',
type: {name:'enum', enums:[{value:'min',label:'Min'},
{value:'max',label:'Max'},
{value:'sum',label:'Sum'},
{value:'avg',label:'Avg'}],allowEditOnly:true},
default:'avg',
displayName: 'Operation',
group: a
})
ports.push({
name: 'agg-' + a,
plug: 'output',
type: 'number',
displayName: a,
group: 'Aggregates'
})
}
else if(schema && schema.type === 'String') {
// String aggregate
ports.push({
index:102,
name: 'aggop-' + a,
plug: 'input',
type: {name:'enum', enums:[{value:'distinct',label:'Distinct'}],allowEditOnly:true},
default:'distinct',
displayName: 'Operation',
group: a
})
ports.push({
name: 'agg-' + a,
plug: 'output',
type: 'string',
displayName: a,
group: 'Aggregates'
})
}
}
})
}
}
// 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'
});
}
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: AggregateNode,
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' || event.name.startsWith('agg')) {
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.noodl.cloud.aggregate', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('noodl.cloud.aggregate')) {
_managePortsForNode(node);
}
});
}
};

View File

@@ -0,0 +1,10 @@
import NoodlRuntime from '@noodl/runtime';
export function registerNodes(runtime: NoodlRuntime) {
[require('./cloud/request'),
require('./cloud/response'),
require('./data/aggregatenode')]
.forEach(function (nodeDefinition) {
runtime.registerNode(nodeDefinition);
});
}

View File

@@ -0,0 +1,36 @@
'use strict';
const Model = require('@noodl/runtime/src/model');
const NoodlRuntime = require('@noodl/runtime');
const JavascriptNodeParser = require('@noodl/runtime/src/javascriptnodeparser');
//Cloud functions override some of the JavascriptNodeParser functions
//Override getComponentScopeForNode to just return an empty object. This basically disabled the 'Component' API in Function/Script nodes and removes a massive memory leak
//Also improves performance.
const componentScope = {};
JavascriptNodeParser.getComponentScopeForNode = function () {
return componentScope;
};
//override the Noodl API so it uses a model scope
JavascriptNodeParser.createNoodlAPI = function (modelScope) {
return {
getProjectSettings: NoodlRuntime.instance.getProjectSettings.bind(NoodlRuntime.instance),
getMetaData: NoodlRuntime.instance.getMetaData.bind(NoodlRuntime.instance),
Object: modelScope || Model,
Variables: (modelScope || Model).get('--ndl--global-variables'),
Records: require('@noodl/runtime/src/api/records')(modelScope),
Users: require('./api/users')(modelScope),
// CloudFunctions: require('./api/cloudfunctions'),
Files: require('./api/files'),
Objects: new Proxy(modelScope || Model, {
get(target, prop, receiver) {
return (modelScope || Model).get(prop);
},
set(obj, prop, value) {
(modelScope || Model).get(prop).setAll(value);
}
})
};
};

View File

@@ -0,0 +1,179 @@
import { CloudRunner } from '.';
let _runner
const _api_response_handlers = {}
_noodl_api_response = (token,res) => {
if(typeof _api_response_handlers[token] === 'function') {
_api_response_handlers[token](res)
delete _api_response_handlers[token]
}
}
const eventQueue = []
_noodl_process_jobs = () => {
while(eventQueue.length > 0) {
const cb = eventQueue.shift()
cb()
}
}
const _defineAPI = () => {
global.require = function(module) {
console.log("Error, require not supported: " + module)
}
global.console = {
log:function() {
let text = "";
for(let i = 0; i < arguments.length; i++)
text += arguments[i] + '\n'
_noodl_api_call('log',undefined,{level:'info',message:text})
},
info:function() {
global.console.log.apply(undefined,arguments)
},
error:function() {
let text = "";
for(let i = 0; i < arguments.length; i++)
text += arguments[i] + '\n'
_noodl_api_call('log',undefined,{level:'error',message:text})
}
}
// ------------------------- Fetch API --------------------------------
class Headers {
constructor(headers) {
this._headers = headers;
}
append(key,value) {
const _key = key.toLowerCase();
this._headers[_key] = this._headers[_key]?this._headers[_key].concat([value]):[value]
}
set(key,value) {
const _key = key.toLowerCase();
this._headers[_key] = [value]
}
get(key) {
const _key = key.toLowerCase();
if(this._headers[_key] === undefined) return null
return this._headers[_key].join(', ');
}
delete(key) {
const _key = key.toLowerCase();
delete this._headers[_key]
}
has(key) {
const _key = key.toLowerCase();
return this._headers[key] !== undefined
}
keys() {
return Object.keys(this._headers)
}
forEach(callback, thisArg = undefined) {
for (const name of this.keys()) {
Reflect.apply(callback, thisArg, [this.get(name), name, this]);
}
}
* values() {
for (const name of this.keys()) {
yield this.get(name);
}
}
* entries() {
for (const name of this.keys()) {
yield [name, this.get(name)];
}
}
}
global.fetch = async function(url,args) {
return new Promise((resolve,reject) => {
const token = Math.random().toString(26).slice(2)
_api_response_handlers[token] = (res) => {
if(res.error === undefined) {
res.json = () => {
try {
return Promise.resolve(JSON.parse(res.body))
}
catch(e) {
return Promise.reject('Failed to parse JSON response')
}
}
res.text = () => {
return Promise.resolve(res.body)
}
res.headers = new Headers(res.headers)
resolve(res)
}
else reject(res.error)
}
_noodl_api_call("fetch",token,{url,...args})
})
}
global.setTimeout = function(cb,millis) {
const token = Math.random().toString(26).slice(2)
_api_response_handlers[token] = () => {
cb()
}
_noodl_api_call("setTimeout",token,millis)
}
global.setImmediate = function(cb) {
eventQueue.push(cb)
_noodl_request_process_jobs()
}
}
const _prepareCloudRunner = async () => {
if(!_runner) {
_runner = new CloudRunner({});
if(typeof _exportedComponents === 'undefined') {
throw Error("No cloud components present.")
}
await _runner.load(_exportedComponents)
}
}
const handleRequest = (req) => {
return new Promise((resolve,reject) => {
_prepareCloudRunner().then(() => {
_runner.run(req.function,{
body:req.body,
headers:req.headers,
}).then(resolve).catch(reject)
}).catch(reject)
})
}
_noodl_handleReq = (token,req) => {
// req.function
// req.headers
// req.body
if(!global.log) {
_defineAPI()
}
console.info(`Cloud function ${req.function} called (requestId:${token})`)
handleRequest(req).then(r => {
console.info(`Cloud function ${req.function} response [${r.statusCode}] (requestId:${token})`)
_noodl_response(token,r)
}).catch(e => {
console.error(`Cloud function ${req.function} response [400] message: ${e.message} (requestId:${token})`)
_noodl_response(token,{statusCode:400,body:JSON.stringify({error:e.message})})
})
}

View File

@@ -0,0 +1,121 @@
import { CloudRunner } from '.';
console.log('Noodl Editor Cloud Runtime!');
console.log('Version: ' + _noodl_cloud_runtime_version);
const _runner = new CloudRunner({
webSocketClass: WebSocket,
connectToEditor: true,
editorAddress: 'ws://localhost:8574',
enableDebugInspectors: true
});
_runner.runtime.graphModel.on('editorImportComplete', () => {
ipcRenderer.send('noodl-cf-has-loaded-project');
});
const handleRequest = async (functionName, req) => {
return await _runner.run(functionName, req);
};
const eventQueue = [];
let hasScheduledProcessJobs = false;
const _noodl_process_jobs = () => {
hasScheduledProcessJobs = false;
while (eventQueue.length > 0) {
const cb = eventQueue.shift();
cb();
}
};
const _setImmediate = window.setImmediate;
window.setImmediate = (cb) => {
eventQueue.push(cb);
if (!hasScheduledProcessJobs) {
hasScheduledProcessJobs = true;
_setImmediate(_noodl_process_jobs);
}
};
const _fetch_response_handlers = {};
const _fetch = window.fetch;
window.fetch = function (url, args) {
if (args && args.platform === 'node') {
return new Promise((resolve, reject) => {
// Perform the fetch via the host node platform
const token = Math.random().toString(26).slice(2);
_fetch_response_handlers[token] = (args) => {
if (args.error === undefined) {
const res = {
body: args.body,
status: args.status,
headers: args.headers,
ok: args.status === 200 || args.status === 201
};
res.json = () => {
try {
return Promise.resolve(JSON.parse(res.body));
} catch (e) {
return Promise.reject('Failed to parse JSON response');
}
};
res.text = () => {
return Promise.resolve(res.body);
};
resolve(res);
} else reject(args.error);
};
ipcRenderer.send('noodl-cf-fetch', {
url,
method: args.method,
token,
headers: JSON.parse(JSON.stringify(args.headers)),
body: args.body
});
});
} else return _fetch(url, args);
};
ipcRenderer.on('noodl-cf-fetch-response', async function (event, args) {
if (typeof _fetch_response_handlers[args.token] === 'function') {
typeof _fetch_response_handlers[args.token](args);
delete _fetch_response_handlers[args.token];
}
});
ipcRenderer.on('noodl-cf-request', async function (event, args) {
if (args.cloudService) {
window._noodl_cloudservices = {
endpoint: args.cloudService.endpoint,
appId: args.cloudService.appId,
masterKey: args.cloudService.masterKey
};
}
console.info(`Cloud function ${args.function} called`);
try {
// args.function
// args.headers
// args.body
const res = await handleRequest(args.function, {
body: args.body,
headers: args.headers
});
console.info(`Cloud function ${args.function} response [${res.statusCode}]`);
event.sender.send('noodl-cf-response', Object.assign({}, res, { token: args.token }));
} catch (e) {
console.error(`Cloud function ${args.function} response [400] message: ${e.message}`);
event.sender.send('noodl-cf-response', {
token: args.token,
statusCode: 400,
body: JSON.stringify({ error: e.message })
});
}
});

View File

@@ -0,0 +1,193 @@
const NoodlRuntime = require('@noodl/runtime');
const EventEmitter = require('@noodl/runtime/src/events');
//const guid = require('../../../guid');
const Model = require('@noodl/runtime/src/model');
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
class UserService {
constructor(modelScope) {
this.events = new EventEmitter();
this.events.setMaxListeners(100000);
// User is fetched and validate when the request is initiated
// see if there is a current user
const request = (modelScope || Model).get('Request');
if (request.UserId !== undefined) {
const user = (modelScope || Model).get(request.UserId);
this.current = user;
}
this.modelScope = modelScope;
}
on() {
this.events.on.apply(this.events, arguments);
}
off() {
this.events.off.apply(this.events, arguments);
}
_makeRequest(path, options) {
if (typeof _noodl_cloudservices === 'undefined') {
options.error && options.error({ error: 'No active cloud service', status: 0 });
return;
}
const cs = _noodl_cloudservices;
fetch(cs.endpoint + path, {
method: options.method || 'GET',
headers: {
'X-Parse-Application-Id': cs.appId,
'X-Parse-Master-Key': cs.masterKey,
'content-type': 'application/json',
'X-Parse-Session-Token': options.sessionToken
},
body: JSON.stringify(options.content)
})
.then((res) => {
if (res.ok) {
res.json().then((json) => options.success(json));
} else {
res.json().then((json) => options.error({ error: json.error, status: res.status }));
}
})
.catch((e) => {
options.error({ error: e.message });
});
}
setUserProperties(options) {
if (this.current !== undefined) {
//make a shallow copy to feed through CloudStore._serializeObject, which will modify the object
const propsToSave = CloudStore._serializeObject({ ...options.properties }, '_User', this.modelScope || Model);
const _content = Object.assign({}, { email: options.email, username: options.username }, propsToSave);
delete _content.createdAt; // Remove props you cannot set
delete _content.updatedAt;
this._makeRequest('/users/' + this.current.getId(), {
method: 'PUT',
content: _content,
success: (response) => {
// Store current user
for (let key in _content) {
this.current.set(key, _content[key]);
}
options.success(response);
},
error: (e) => {
options.error(e.error);
}
});
}
}
logIn(options) {
this._makeRequest('/login', {
method: 'POST',
content: {
username: options.username,
password: options.password,
method: 'GET'
},
success: (user) => {
delete user.ACL;
delete user.className;
delete user.__type;
const _user = CloudStore._fromJSON(user, '_User', this.modelScope || Model);
options.success(_user);
},
error: (e) => {
options.error(e.error);
}
});
}
// Just fetch the user don't set to current
fetchUser(options) {
this._makeRequest('/users/me', {
method: 'GET',
sessionToken: options.sessionToken,
success: (user) => {
// Store current user
delete user.ACL;
delete user.className;
delete user.__type;
const _user = CloudStore._fromJSON(user, '_User', this.modelScope || Model);
options.success(_user);
},
error: (e) => {
options.error(e.error);
}
});
}
fetchCurrentUser(options) {
if (options.sessionToken) {
// Fetch the current user with the session token
this._makeRequest('/users/me', {
method: 'GET',
sessionToken: options.sessionToken,
success: (user) => {
// Store current user
delete user.ACL;
delete user.className;
delete user.__type;
this.current = CloudStore._fromJSON(user, '_User', this.modelScope || Model);
this.events.emit('sessionGained');
options.success(this.current);
},
error: (e) => {
options.error(e.error);
}
});
} else if (this.current !== undefined) {
// Fetch the current user, will use master key
this._makeRequest('/users/' + this.current.getId(), {
method: 'GET',
success: (user) => {
// Store current user
delete user.ACL;
delete user.className;
delete user.__type;
this.current = CloudStore._fromJSON(user, '_User', this.modelScope || Model);
options.success(this.current);
},
error: (e) => {
options.error(e.error);
}
});
}
}
}
UserService.forScope = (modelScope) => {
if (modelScope === undefined) return UserService.instance;
if (modelScope._userService) return modelScope._userService;
modelScope._userService = new UserService(modelScope);
return modelScope._userService;
};
var _instance;
Object.defineProperty(UserService, 'instance', {
get: function () {
if (_instance === undefined) _instance = new UserService();
return _instance;
}
});
NoodlRuntime.Services.UserService = UserService;
module.exports = UserService;

View File

@@ -0,0 +1,507 @@
/* eslint-disable */
declare namespace Noodl {
function getProjectSettings(): any;
function getMetaData(): any;
interface VariablesApi {
[K in VariableNames]: any
}
/**
* You can access all variables in your application trough the Noodl.Variables object.
* Changing a variable will trigger all connections to be updated for all Variable nodes
* in your project with the corresponding variable name.
*
* Example:
* ```ts
* // This will change the variable named MyVariable
* // and trigger all variable nodes in your project
* Noodl.Variables.MyVariable = "Hello";
*
* // Use this if you have spaces in your variable name
* Noodl.Variables["My Variable"] = 10;
*
* Noodl.Variables.userName = "Mickeeeey";
*
* // Reading variables
* console.log(Noodl.Variables.userName);
* ```
*
* {@link https://docs.noodl.net/#/javascript/reference/variables}
*/
const Variables: VariablesApi;
/**
* One step above Variables are Objects, this is a global data model of Noodl objects.
* Each object is referenced with an Id and can contain a set of properties.
* You can access all objects in your workspace through their Id and the Noodl.Objects prefix.
* Change a property of an object will trigger all connections
* from object nodes with the corresponding Id and property.
*
* Example:
* ```ts
* // This will change the property MyProperty
* // of object with id MyObjectId and trigger
* // all object nodes (with that id) in your project
* Noodl.Objects.MyObjectId.MyProperty = "Hello";
*
* // Use this notation of that object id contains spaces
* Noodl.Objects["Form Values"].input_text = "Whoops";
*
* Noodl.Objects["Form Values"]["A property with spaces"] = 20;
*
* // Reading an object property
* console.log(Noodl.Objects.CurrentUser.Name);
*
* // This will set all properties of the object you assign with
* // to the object with id "SomeId"
* // You cannot set the id property this way,
* // that property will be ignored if part of the object you assign
* Noodl.Objects.SomeId = {
* // ...
* }
* ```
*
* {@link https://docs.noodl.net/#/javascript/reference/objects}
*/
const Objects: any;
/**
* Allows access to Object from Javascript.
*
* {@link https://docs.noodl.net/#/javascript/reference/object}
*/
const Object: any;
interface RecordsApi {
/**
* This is an async function that will query the database using the query
* that you provide and return the result or throw an exception if failed.
*
* The query parameter has the same format as the advanced query of the Query Records node.
* {@link https://docs.noodl.net/#/nodes/data/cloud-data/query-records/#advanced-filters}
*
* Example:
* ```ts
* const results = await Noodl.Records.query("myClass", {
* Completed: { equalTo: true },
* })
* ```
*
* The result is an array of Noodl.Object. The options can be used to specify sorting,
* it also follows the same pattern as the advanced filters of the Query Records node.
*
* Example:
* ```ts
* const results = await Noodl.Records.query("myClass", {
* Completed: { equalTo: true },
* }, {
* sort:['createdAt']
* })
* ```
*
* You can also specify the limit for how many records to return as a maximum (defaults to 100)
* with the limit option, and if you want the returned records to start from a given
* index specify the skip option.
*
* ```ts
* const results = await Noodl.Records.query("myClass", {
* Completed: { equalTo: true },
* }, {
* sort: ['-createdAt'], // use - to sort descending
* skip: 50,
* limit: 200
* })
* ```
*/
query(
className: RecordClassName,
query?: any,
options?: {
limit?: number;
skip?: number;
sort?: string[];
include?: any;
select?: any;
}
): Promise<any>;
/**
* This function returns the count of the number of records of a given class,
* optionally matching a query filter.
*
* Example:
* ```ts
* // The number of records of myClass in the database
* const count = await Noodl.Records.count("myClass")
*
* // The number of myClass records in the database that match a query
* const completedCount = await Noodl.Records.count("myClass", {
* Completed: { equalTo: true },
* })
* ```
*/
count(className: RecordClassName, query?: any): Promise<number>;
/**
* returns an array of unique values for a given propery or all records in
* the database of a given class. Optionally you can suppoly a query filter.
*/
distinct(
className: RecordClassName,
property: string,
query: any
): Promise<any>;
/**
* Use this function to fetch the latest properties of a specific record
* from the cloud database. It will return the object / record.
*
* Example:
* ```ts
* // If you use the a record ID you must also specify the class
* const myRecord = await Noodl.Records.fetch(myRecordId, { className: "myClass" })
*
* // You can also fetch a record you have previously fetched or received from a
* // query, to get the latest properties from the backend
* await Noodl.Records.fetch(myRecord)
* ```
*/
fetch(
objectOrId: string | { getId(): string; },
options?: {
className?: RecordClassName;
}
): Promise<any>;
/**
* Use this function to write an existing record to the cloud database.
* It will attempt to save all properties of the record / object
* if you don't specify the optional properties argument,
* if so it will set and save those properties.
*
* Example:
* ```ts
* Noodl.Objects[myRecordId].SomeProperty = "hello"
*
* // If you use the record id to save, you need to specify the classname explicitly
* // by specfiying null or undefinded for properties it will save all proporties in
* // the record
* await Noodl.Records.save(myRecordId, null, { className: "myClass" })
*
* // Or use the object directly
* await Noodl.Records.save(Noodl.Objects[myRecordId])
*
* // Set specified properties and save only those to the backned
* await Noodl.Records.save(myRecord, {
* SomeProperty:'hello'
* })
* ```
*/
save(
objectOrId: string | { getId(): string; },
properties: any,
options?: {
className?: RecordClassName;
acl?: any;
}
): Promise<void>;
/**
* This function will increment (or decrease) propertis of a certain record
* saving it to the cloud database in a race condition safe way.
* That is, normally you would have to first read the current value,
* modify it and save it to the database. Here you can do it with one operation.
*
* Example:
* ```ts
* // Modify the specified numbers in the cloud
* await Noodl.Records.increment(myRecord, {
* Score: 10,
* Life: -1
* })
*
* // Like save, you can use a record Id and class
* await Noodl.Records.save(myRecordId, { Likes: 1 }, { className: "myClass" })
* ```
*
* Using the options you can also specify access control as described in this guide,
* this let's you control which users can access a specific record.
* The access control is specified as below:
* ```ts
* await Noodl.Records.save(myRecord, null, {
* acl: {
* // "*" means everyone, this rules gives everyone read access but not write
* "*": { read: true, write: false },
* // give a specific user write access
* "a-user-id": { read: true, write: true },
* // give a specific role write access
* "role:a-role-name": { read: true, write: true },
* }
* })
* ```
*/
increment(
objectOrId: string | { getId(): string; },
properties: any,
options?: {
className?: RecordClassName;
}
): Promise<void>;
/**
* This function will create a new record in the cloud database and return
* the Noodl.Object of the newly created record.
* If it's unsuccessful it will throw an exception.
*
* Example:
* ```ts
* const myNewRecord = await Noodl.Records.create("myClass",{
* SomeProperty:"Hello"
* })
*
* console.log(myNewRecord.SomeProperty)
* ```
*
* You can use the options agrument to specify access control rules
* as detailed under Noodl.Records.save above.
*/
create(
className: RecordClassName,
properties: any,
options?: {
acl?: any
}
): Promise<any>;
/**
* Use this function to delete an existing record from the cloud database.
*
* Example:
* ```ts
* // If you specify the id of a record to be deleted, you must also provide the
* // class name in the options
* await Noodl.Records.delete(myRecordId,{className:"myClass"})
*
* // Or use the object directly (provided it was previously fetched or received via a query)
* await Noodl.Records.delete(Noodl.Objects[myRecordId])
* ```
*/
delete(
objectOrId: string | { getId(): string; },
options?: {
className?: RecordClassName;
}
): Promise<void>;
/**
* Use this function to add a relation between two records.
*
* Example:
* ```ts
* // You can either specify the Ids and classes directly
* await Noodl.Records.addRelation({
* className: "myClass",
* recordId: "owning-record-id",
* key: "the-relation-key-on-the-owning-record",
* targetRecordId: "the-id-of-the-record-to-add-a-relation-to",
* targetClassName: "the-class-of-the-target-record"
* })
*
* // Or if you already have two records that have been previously fetched or returned from a
* // query
* await Noodl.Records.addRelation({
* record: myRecord,
* key: 'relation-key',
* targetRecord: theTargetRecord
* })
* ```
*/
addRelation(
options: {
recordId: string | { getId(): string; },
className?: RecordClassName,
key: string,
targetRecordId: string | { getId(): string; },
targetClassName?: RecordClassName
}
): Promise<void>;
/**
* Use this function to remove a relation between two records.
*
* ```ts
* // You can either specify the Ids and classes directly
* await Noodl.Records.removeRelation({
* className: "myClass",
* recordId: "owning-record-id",
* key: "the-relation-key-on-the-owning-record",
* targetRecordId: "the-id-of-the-record-to-remove-a-relation-to",
* targetClassName: "the-class-of-the-target-record"
* })
*
* // Or if you already have two records that have been previously fetched or returned from a
* // query
* await Noodl.Records.removeRelation({
* record: myRecord,
* key: 'relation-key',
* targetRecord: theTargetRecord
* })
* ```
*/
removeRelation(
options: {
recordId: string | { getId(): string; },
className?: RecordClassName,
key: string,
targetRecordId: string | { getId(): string; },
targetClassName?: RecordClassName
}
): Promise<void>;
/**
* compute a set of aggregates based on properties in the records.
* It can be limited with a query.
*
* You can use the following aggregate functions:
* - sum Compute the sum of a number property access matching records.
* - min Compute the minimum value of a number property access matching records.
* - max Compute the maximum value of a number property access matching records.
* - avg Compute the average value of a number property access matching records.
*/
aggregate(
className: RecordClassName,
aggregates: any,
query: any
): Promise<any>;
}
/**
* With Records you can query, read and write records to the cloud database.
* All functions are async and will throw an exception if they fail.
*
* Example:
* ```ts
* try {
* await Noodl.Records.delete(myRecord)
* }
* catch(e) {
* console.log(e)
* }
* ```
*
* {@link https://docs.noodl.net/#/javascript/reference/records}
*/
const Records: RecordsApi;
interface CurrentUserObject {
UserId: string;
// TODO: Fill in the User Record here.
Properties: Record<string, any>;
/**
* Attempt to save the properties of the current user.
*
* If you have made changes to the current() user object you will need
* to call this function to write the changes to the backend.
*/
save(): Promise<void>;
/**
* Fetch that laters properties of the user object from the cloud database.
* It will throw an exception if the user session has expired.
*/
fetch(): Promise<void>;
}
interface UsersApi {
/**
* Attempt to login to create a user session.
*
* After a successful login you can access the user object with `Noodl.Users.Current`.
*/
logIn(options: { username: string; password: string }): Promise<any>;
/**
* Get a session token for a user that you can later send to the client to log that user in.
* This does not require a password and must be runon a cloud function
* (since they all have full access to the database).
*
* You can provide a duration for the session,
* or it will expire after 24 hours as default.
*
* If successful this call will return a user object that contains a session token
* that you can return to the client and use with the become function.
*
* __installationId__ is an optional that is a unique id for the client if you don't want
* to share sessions between different clients. Most common is to generate a random id
* on the client and pass to the cloud function when you are logging in.
*/
impersonate(username: string, options?: { duration?: number, installationID?: string }): Promise<any>;
/**
* Return the current user object and properties if one exists.
*/
get Current(): CurrentUserObject | undefined;
}
/**
* The Noodl.Users object let's you access the current session user.
*
* {@link https://docs.noodl.net/#/javascript/reference/users}
*/
const Users: UsersApi;
interface FilesApi {
/**
* Delete a file that has been uploaded to the backend.
* You need to provide the file name that was returned when the file was uploaded.
* So not the full url but the hash+filename returned by the upload function.
*/
delete(filename: string): Promise<void>;
}
/**
* The Noodl.Files service lets you access the cloud services files.
*
* {@link https://docs.noodl.net/#/javascript/reference/files}
*/
const Files: FilesApi;
}
interface ComponentApi {
/**
* `Component.Object` is the Component Object of the current component and
* you can use it just like any other Noodl.Object.
* Most commonly this means accessing the properties of the object.
* When you set a property any Component Object node in this component
* instance will update accordingly.
*/
Object: any;
/**
* Object is the Parent Component Object,
* that is the Component Object of the parent component in the visual heirarchy.
* It is also used like any other Noodl.Object.
*/
ParentObject: any;
/**
* If this component is the template of a repeater this will contain
* the object of the items array corresponding to this specific component instance.
* That is the same object as if you set an object Id Source to From Repeater, as shown below.
*/
RepeaterObject: any;
}
/**
* The `Component` object is ony available in Function and Script nodes and
* it contains things related to the component scope where the
* Function or Script node is executing.
*
* {@link https://docs.noodl.net/#/javascript/reference/component}
*/
declare const Component: ComponentApi;

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Noodl Editor Cloud Runtime</title>
<script src="./sandbox.viewer.bundle.js"></script>
</head>
</html>

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ES2019", "ES2020.String"],
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,9 @@
const path = require('path');
module.exports = {
// Allows to define the output path of the files built by the viewer.
//
// For example in the CLI, we will also build this, just with a different output path.
outPath: process.env.OUT_PATH || path.resolve(__dirname, '../../noodl-editor/src/external'),
runtimeVersion: 'cloud-runtime-' + require('../package.json').version.replaceAll('.', '-')
};

View File

@@ -0,0 +1,22 @@
//config shared for both regular viewer and deploy versions
module.exports = {
externals: {},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
};

View File

@@ -0,0 +1,4 @@
const viewer = require('./webpack.viewer.dev');
const isolate = require('./webpack.isolate.dev');
module.exports = [viewer, isolate];

View File

@@ -0,0 +1,34 @@
const path = require('path');
const { runtimeVersion } = require('./constants.js');
const webpack = require('webpack');
const prefix = `const _noodl_cloud_runtime_version = "${runtimeVersion}";`;
module.exports = {
//mode: 'production',
mode: 'development',
watch: true,
entry: './src/sandbox.isolate.js',
target: 'node',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.ts', '.js']
},
output: {
path: path.resolve(__dirname, '../dist')
},
plugins: [
new webpack.BannerPlugin({
banner: prefix,
raw: true
})
]
};

View File

@@ -0,0 +1,32 @@
const path = require('path');
const { runtimeVersion } = require('./constants.js');
const webpack = require('webpack');
const prefix = `const _noodl_cloud_runtime_version = "${runtimeVersion}";`;
module.exports = {
mode: 'production',
entry: './src/sandbox.isolate.js',
target: 'node',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.ts', '.js']
},
output: {
path: path.resolve(__dirname, '../dist')
},
plugins: [
new webpack.BannerPlugin({
banner: prefix,
raw: true
})
]
};

View File

@@ -0,0 +1,4 @@
const viewer = require('./webpack.viewer.prod');
const isolate = require('./webpack.isolate.prod');
module.exports = [viewer, isolate];

View File

@@ -0,0 +1,49 @@
//shared config for regular (non-deploy) viewer
const path = require('path');
const { merge } = require('webpack-merge');
const { outPath, runtimeVersion } = require('./constants.js');
const common = require('./webpack.common.js');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const GenerateJsonPlugin = require('generate-json-webpack-plugin');
const noodlEditorExternalViewerPath = path.join(outPath, 'cloudruntime');
function stripStartDirectories(targetPath, numDirs) {
const p = targetPath.split('/');
p.splice(0, numDirs);
return p.join(path.sep);
}
const prefix = `const { ipcRenderer } = require('electron'); const _noodl_cloud_runtime_version = "${runtimeVersion}";`;
module.exports = merge(common, {
entry: {
sandbox: './src/sandbox.viewer.js'
},
output: {
filename: 'sandbox.viewer.bundle.js',
path: noodlEditorExternalViewerPath
},
plugins: [
new webpack.BannerPlugin({
banner: prefix,
raw: true
}),
new CleanWebpackPlugin(noodlEditorExternalViewerPath, {
allowExternal: true
}),
new CopyWebpackPlugin([
{
from: 'static/viewer/**/*',
transformPath: (targetPath) => stripStartDirectories(targetPath, 2)
}
]),
new GenerateJsonPlugin('manifest.json', {
version: runtimeVersion
})
]
});

View File

@@ -0,0 +1,8 @@
const { merge } = require("webpack-merge");
const common = require("./webpack.viewer.common.js");
module.exports = merge(common, {
mode: "development",
devtool: "inline-source-map",
watch: true,
});

View File

@@ -0,0 +1,6 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.viewer.common.js');
module.exports = merge(common, {
mode: 'production'
});