diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..30160b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +To help us understand the issue, please fill-in as much of the following information as you can: + +### What are the steps to reproduce? + +### What happens? + +### What do you expect to happen? + +### Please tell us about your environment: + +- [ ] Node-RED version: +- [ ] node.js version: +- [ ] npm version: +- [ ] Platform/OS: +- [ ] Browser: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9091f94 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +## Types of changes + +What types of changes does your code introduce? +_Put an `x` in the boxes that apply_ + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) + +## Proposed changes + +Describe the nature of this change. What problem does it address? + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c291f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Generated nodes +node-red-contrib-* +node-red-node-* + +# Dependency directory +node_modules diff --git a/README.md b/README.md index 0ecccab..67c6378 100644 --- a/README.md +++ b/README.md @@ -1 +1,53 @@ -# node-red-nodegen \ No newline at end of file +# Node generator for Node-RED + +Install node generator globally to make the `node-red-nodegen` command available on your path: + + npm install -g git+http://github.com/node-red/node-red-nodegen.git + +You may need to run this with `sudo`, or from within an Administrator command shell. + +## Usage + + Usage: + node-red-nodegen [-o ] [--prefix ] [--name ] [--module ] [--version [--tgz] [--help] + + Description: + Node generator for Node-RED + + Supported source: + - Function node (js file in library, "~/.node-red/lib/function/") + - Swagger definition + + Options: + -o : Destination path to save generated node (default: current directory) + --prefix : Prefix of npm module (default: "node-red-contrib-") + --name : Node name (default: name defined in source) + --module : Module name (default: "node-red-contrib-") + --version : Node version (format: "number.number.number" like "4.5.1") + --tgz : Save node as tgz file + --help : Show help + +### Example 1. Create original node from function node (JavaScript code) + +- On Node-RED flow editor, save function node to library with file name (lower-case.js). +- node-red-nodegen ~/.node-red/lib/function/lower-case.js +- cd node-red-contrib-lower-case +- sudo npm link +- cd ~/.node-red +- npm link node-red-contrib-lower-case +- node-red + +-> You can use lower-case node on Node-RED flow editor. + +### Example 2. Create original node from Swagger definition + +- node-red-nodegen http://petstore.swagger.io/v2/swagger.json +- cd node-red-contrib-swagger-petstore +- sudo npm link +- cd ~/.node-red +- npm link node-red-contrib-swagger-petstore +- node-red + +-> You can use swagger-petstore node on Node-RED flow editor. + +Note: Currently node generator supports GET and POST methods using JSON format without authentication. diff --git a/bin/node-red-nodegen.js b/bin/node-red-nodegen.js new file mode 100644 index 0000000..4b4cc34 --- /dev/null +++ b/bin/node-red-nodegen.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var fs = require('fs'); +var request = require('request'); +var yamljs = require('yamljs'); +var argv = require('minimist')(process.argv.slice(2)); +var colors = require('colors'); +var nodegen = require('../lib/nodegen.js'); + +// Command options +var options = {}; +options.tgz = argv.tgz; +options.obfuscate = argv.obfuscate; + +var data = { + prefix: argv.prefix || argv.p, + name: argv.name || argv.n, + module: argv.module, + version: argv.version || argv.v, + dst: argv.output || argv.o || '.' +}; + +function help() { + var helpText = 'Usage:'.bold + '\n' + + ' node-red-nodegen ' + + ' [-o ]' + + ' [--prefix ]' + + ' [--name ]' + + ' [--module ]' + + ' [--version ' + + //' [--icon ' + + //' [--color ' + + ' [--tgz]' + + ' [--help]\n' + + '\n' + + 'Description:'.bold + '\n' + + ' Node generator for Node-RED\n' + + '\n' + + 'Supported source:'.bold + '\n' + + ' - Function node (js file in library, "~/.node-red/lib/function/")\n' + + // ' - Subflow node (json file of subflow)\n' + + ' - Swagger definition\n' + + '\n' + + 'Options:\n'.bold + + ' -o : Destination path to save generated node (default: current directory)\n' + + ' --prefix : Prefix of npm module (default: "node-red-contrib-")\n' + + ' --name : Node name (default: name defined in source)\n' + + ' --module : Module name (default: "node-red-contrib-")\n' + + ' --version : Node version (format: "number.number.number" like "4.5.1")\n' + + //' --icon : png or gif file for node appearance (image size should be 10x20)\n'; + //' --color : color for node appearance (format: color hexadecimal numbers like "#A6BBCF")\n'; + ' --tgz : Save node as tgz file\n' + + ' --help : Show help\n'; + console.log(helpText); +} + +if (!argv.h && !argv.help) { + var sourcePath = argv._[0]; + if (sourcePath) { + if (sourcePath.startsWith('http://') || sourcePath.startsWith('https://')) { + request(sourcePath, function (error, response, body) { + if (!error) { + data.src = JSON.parse(body); + var filename = nodegen.swagger2node(data, options); + console.log('Success: ' + filename); + } else { + console.error(error); + } + }); + } else if (sourcePath.endsWith('.json')) { + data.src = JSON.parse(fs.readFileSync(sourcePath)); + var filename = nodegen.swagger2node(data, options); + console.log('Success: ' + filename); + } else if (sourcePath.endsWith('.yaml')) { + data.src = yamljs.load(sourcePath); + var filename = nodegen.swagger2node(data, options); + console.log('Success: ' + filename); + } else if (sourcePath.endsWith('.js')) { + data.src = fs.readFileSync(sourcePath); + var filename = nodegen.function2node(data, options); + console.log('Success: ' + filename); + } else { + console.error('error: Unsupported file type'); + } + } else { + help(); + } +} else { + help(); +} + diff --git a/lib/nodegen.js b/lib/nodegen.js new file mode 100644 index 0000000..07b1062 --- /dev/null +++ b/lib/nodegen.js @@ -0,0 +1,382 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var fs = require('fs'); +var child_process = require('child_process'); +var request = require('request'); +var mustache = require('mustache'); +var jsStringEscape = require('js-string-escape'); +var obfuscator = require('javascript-obfuscator'); +var CodeGen = require('swagger-js-codegen').CodeGen; + +function createCommonFiles(templateDirectory, data) { + // Make directories + try { + fs.mkdirSync(data.dst + '/' + data.module); + } catch (error) { + if (error.code !== 'EEXIST') { + console.error(error); + } + } + try { + fs.mkdirSync(data.dst + '/' + data.module + '/icons'); + } catch (error) { + if (error.code !== 'EEXIST') { + console.error(error); + } + } + try { + fs.mkdirSync(data.dst + '/' + data.module + '/locales'); + } catch (error) { + if (error.code !== 'EEXIST') { + console.error(error); + } + } + try { + var languages = fs.readdirSync(templateDirectory + '/locales'); + languages.forEach(function (language) { + try { + fs.mkdirSync(data.dst + '/' + data.module + '/locales/' + language); + } catch (error) { + if (error.code !== 'EEXIST') { + console.error(error); + } + } + }); + } catch (error) { + if (error.code !== 'ENOENT') { + console.error(error); + } + } + + // Create icon.png + fs.createReadStream(templateDirectory + '/icons/icon.png').pipe(fs.createWriteStream(data.dst + '/' + data.module + '/icons/icon.png')); +} + +function runNpmPack(data) { + var npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + child_process.execFile(npmCommand, ['pack', data.module], { cwd: data.dst }, + function (error) { + if (error) { + console.error(error); + } + }); +} + +function function2node(data, options) { + // Read meta data in js file + var meta = {}; + var parts = new String(data.src).split('\n'); + parts.forEach(function (part) { + var match = /^\/\/ (\w+): (.*)/.exec(part.toString()); + if (match) { + if (match[1] === 'name') { + meta.name = match[2].replace(/([A-Z])/g, ' $1').toLowerCase().replace(/[^ a-z0-9]+/g, '').replace(/^ | $/, '').replace(/ +/g, '-'); + } else { + meta[match[1]] = match[2]; + } + } + }); + + if (!data.name || data.name === '') { + data.name = meta.name; + } + + if (data.module) { + if (data.prefix) { + console.error('error: module name and prefix are conflicted.'); + } + } else { + if (data.prefix) { + data.module = data.prefix + data.name; + } else { + data.module = 'node-red-contrib-' + data.name; + } + } + + if (!data.version || data.version === '') { + data.version = '0.0.1'; + } + + if (data.name === 'function') { + console.error('\'function\' is duplicated node name. Use another name.'); + } else { + var params = { + nodeName: data.name, + projectName: data.module, + projectVersion: data.version, + func: jsStringEscape(data.src), + outputs: meta.outputs + }; + + createCommonFiles(__dirname + '/../templates/function', data); + + // Create package.json + var packageTemplate = fs.readFileSync(__dirname + '/../templates/function/package.json.mustache', 'utf-8'); + var packageSourceCode = mustache.render(packageTemplate, params); + fs.writeFileSync(data.dst + '/' + data.module + '/package.json', packageSourceCode); + + // Create node.js + var nodeTemplate = fs.readFileSync(__dirname + '/../templates/function/node.js.mustache', 'utf-8'); + var nodeSourceCode = mustache.render(nodeTemplate, params); + if (options.obfuscate) { + nodeSourceCode = obfuscator.obfuscate(nodeSourceCode, { stringArrayEncoding: 'rc4' }); + } + fs.writeFileSync(data.dst + '/' + data.module + '/node.js', nodeSourceCode); + + // Create node.html + var htmlTemplate = fs.readFileSync(__dirname + '/../templates/function/node.html.mustache', 'utf-8'); + var htmlSourceCode = mustache.render(htmlTemplate, params); + fs.writeFileSync(data.dst + '/' + data.module + '/node.html', htmlSourceCode); + + // Create README.md + var readmeTemplate = fs.readFileSync(__dirname + '/../templates/function/README.md.mustache', 'utf-8'); + var readmeSourceCode = mustache.render(readmeTemplate, params); + fs.writeFileSync(data.dst + '/' + data.module + '/README.md', readmeSourceCode); + + // Create LICENSE file + var licenseTemplate = fs.readFileSync(__dirname + '/../templates/function/LICENSE.mustache', 'utf-8'); + var licenseSourceCode = mustache.render(licenseTemplate, params); + fs.writeFileSync(data.dst + '/' + data.module + '/LICENSE', licenseSourceCode); + + if (options.tgz) { + runNpmPack(data); + return data.dst + '/' + data.module + '-' + data.version + '.tgz'; + } else { + return data.dst + '/' + data.module; + } + } +} + +function swagger2node(data, options) { + // Modify swagger data + var swagger = JSON.parse(JSON.stringify(data.src), function (key, value) { + if (key === 'consumes') { // 1. Filter type for 'Content-Type' in request header + return undefined; + } else if (key === 'produces') { // 2. Filter type for 'Accept' in request header + if (value.indexOf('application/json') >= 0) { + return ['application/json']; + } + if (value.indexOf('application/xml') >= 0) { + return ['application/xml']; + } else { + return [value[0]]; + } + } else if (key === 'default') { // 3. Handle swagger-js-codegen bug + return undefined; + } else { + return value; + } + }); + + var className = swagger.info.title.toLowerCase().replace(/(^|[^a-z0-9]+)[a-z0-9]/g, + function (str) { + return str.replace(/^[^a-z0-9]+/, '').toUpperCase(); + }); + + if (!data.name || data.name === '') { + data.name = className.replace(/([A-Z])/g, '-$1').slice(1).toLowerCase(); + } + + if (data.module) { + if (data.prefix) { + console.error('error: module name and prefix are conflicted.'); + } + } else { + if (data.prefix) { + data.module = data.prefix + data.name; + } else { + data.module = 'node-red-contrib-' + data.name; + } + } + + if (!data.version || data.version === '') { + if (swagger.info.version) { + var version = swagger.info.version.replace(/[^0-9\.]/g, ''); + if (version.match(/^[0-9]+$/)) { + data.version = version + '.0.0'; + } else if (version.match(/^[0-9]+\.[0-9]+$/)) { + data.version = version + '.0'; + } else if (version.match(/^[0-9]+\.[0-9]+\.[0-9]+$/)) { + data.version = version; + } else { + data.version = '0.0.1'; + } + } else { + data.version = '0.0.1'; + } + } + + createCommonFiles(__dirname + '/../templates/swagger', data); + + // Create Node.js SDK + var nodejsSourceCode = CodeGen.getNodeCode({ + className: className, + swagger: swagger, + lint: false, + beautify: false + }); + if (options.obfuscate) { + nodejsSourceCode = obfuscator.obfuscate(nodejsSourceCode, { stringArrayEncoding: 'rc4' }); + } + fs.writeFileSync(data.dst + '/' + data.module + '/lib.js', nodejsSourceCode); + + // Create package.json + var packageSourceCode = CodeGen.getCustomCode({ + className: className, + swagger: swagger, + template: { + class: fs.readFileSync(__dirname + '/../templates/swagger/package.json.mustache', 'utf-8'), + method: '', + type: '' + }, + mustache: { + nodeName: data.name, + projectName: data.module, + projectVersion: data.version, + licenseName: function () { + if (swagger.info.license && swagger.info.license.name) { + return swagger.info.license.name; + } else { + return 'Apache-2.0'; + } + }, + projectAuthor: swagger.info.contact && swagger.info.contact.name ? swagger.info.contact.name : '' + }, + lint: false, + beautify: false + }); + fs.writeFileSync(data.dst + '/' + data.module + '/package.json', packageSourceCode); + + // Create node.js + var nodeSourceCode = CodeGen.getCustomCode({ + className: className, + swagger: swagger, + template: { + class: fs.readFileSync(__dirname + '/../templates/swagger/node.js.mustache', 'utf-8'), + method: '', + type: '' + }, + mustache: { + nodeName: data.name, + }, + lint: false, + beautify: false + }); + if (options.obfuscate) { + nodeSourceCode = obfuscator.obfuscate(nodeSourceCode, { stringArrayEncoding: 'rc4' }); + } + fs.writeFileSync(data.dst + '/' + data.module + '/node.js', nodeSourceCode); + + // Create node.html + var htmlSourceCode = CodeGen.getCustomCode({ + className: className, + swagger: swagger, + template: { + class: fs.readFileSync(__dirname + '/../templates/swagger/node.html.mustache', 'utf-8'), + method: '', + type: '' + }, + mustache: { + nodeName: data.name, + }, + lint: false, + beautify: false + }); + fs.writeFileSync(data.dst + '/' + data.module + '/node.html', htmlSourceCode); + + // Create language files + var languages = fs.readdirSync(__dirname + '/../templates/swagger/locales'); + languages.forEach(function (language) { + var languageFileSourceCode = CodeGen.getCustomCode({ + className: className, + swagger: swagger, + template: { + class: fs.readFileSync(__dirname + '/../templates/swagger/locales/' + language + '/node.json.mustache', 'utf-8'), + method: '', + type: '' + }, + mustache: { + nodeName: data.name, + }, + lint: false, + beautify: false + }); + fs.writeFileSync(data.dst + '/' + data.module + '/locales/' + language + '/node.json', JSON.stringify(JSON.parse(languageFileSourceCode), null, 4)); + }); + + // Create README.md + var readmeSourceCode = CodeGen.getCustomCode({ + className: className, + swagger: swagger, + template: { + class: fs.readFileSync(__dirname + '/../templates/swagger/README.md.mustache', 'utf-8'), + method: '', + type: '' + }, + mustache: { + nodeName: data.name, + projectName: data.module, + }, + lint: false, + beautify: false + }); + fs.writeFileSync(data.dst + '/' + data.module + '/README.md', readmeSourceCode); + + // Create LICENSE file + var licenseSourceCode = CodeGen.getCustomCode({ + projectName: data.module, + className: className, + swagger: swagger, + template: { + class: fs.readFileSync(__dirname + '/../templates/swagger/LICENSE.mustache', 'utf-8'), + method: '', + type: '' + }, + mustache: { + licenseName: function () { + if (swagger.info.license && swagger.info.license.name) { + return swagger.info.license.name; + } else { + return 'Apache-2.0'; + } + }, + licenseUrl: function () { + if (swagger.info.license && swagger.info.license.url) { + return swagger.info.license.url; + } else { + return ''; + } + } + }, + lint: false, + beautify: false + }); + fs.writeFileSync(data.dst + '/' + data.module + '/LICENSE', licenseSourceCode); + + if (options.tgz) { + runNpmPack(data); + return data.dst + '/' + data.module + '-' + data.version + '.tgz'; + } else { + return data.dst + '/' + data.module; + } +} + +module.exports = { + function2node: function2node, + swagger2node: swagger2node +}; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..122fb2a --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "node-red-nodegen", + "version": "0.0.1", + "description": "Node generator for Node-RED", + "homepage": "http://nodered.org", + "main": "./lib/nodegen.js", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/node-red/node-red-nodegen.git" + }, + "engines": { + "node": ">=6" + }, + "contributors": [ + { + "name": "Kazuhito Yokoi" + }, + { + "name": "Hiroki Uchikawa" + }, + { + "name": "Kazuki Nakanishi" + }, + { + "name": "Hideki Nakamura" + }, + { + "name": "Hiroyasu Nishiyama" + }, + { + "name": "Nick O'Leary" + }, + { + "name": "Dave Conway-Jones" + } + ], + "keywords": [ + "nodegen", + "function", + "swagger", + "codegen", + "swagger-codegen" + ], + "dependencies": { + "request": "2.83.0", + "yamljs": "0.3.0", + "mustache": "2.3.0", + "js-string-escape": "1.0.1", + "javascript-obfuscator": "0.12.2", + "swagger-js-codegen": "1.12.0", + "minimist": "1.2.0", + "colors": "1.1.2" + }, + "bin": { + "node-red-nodegen": "./bin/node-red-nodegen.js" + } +} diff --git a/samples/lower-case.js b/samples/lower-case.js new file mode 100644 index 0000000..051f274 --- /dev/null +++ b/samples/lower-case.js @@ -0,0 +1,4 @@ +// name: lower-case +// outputs: 1 +msg.payload = msg.payload.toLowerCase(); +return msg; diff --git a/templates/function/LICENSE.mustache b/templates/function/LICENSE.mustache new file mode 100644 index 0000000..a548602 --- /dev/null +++ b/templates/function/LICENSE.mustache @@ -0,0 +1,208 @@ +This node is under Apache-2.0 licence because it was created from the following +fuction node code using [node-red-nodegen](https://github.com/node-red/node-red-nodegen). +https://github.com/node-red/node-red/blob/master/nodes/core/core/80-function.html +https://github.com/node-red/node-red/blob/master/nodes/core/core/80-function.js + +--------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/templates/function/README.md.mustache b/templates/function/README.md.mustache new file mode 100644 index 0000000..43aac25 --- /dev/null +++ b/templates/function/README.md.mustache @@ -0,0 +1,14 @@ +{{&projectName}} +===================== + +Node-RED node for {{&nodeName}} + +{{&description}} + +Install +------- + +Run the following command in your Node-RED user directory - typically `~/.node-red` + + npm install {{&projectName}} + diff --git a/templates/function/icons/icon.png b/templates/function/icons/icon.png new file mode 100644 index 0000000..9095050 Binary files /dev/null and b/templates/function/icons/icon.png differ diff --git a/templates/function/node.html.mustache b/templates/function/node.html.mustache new file mode 100644 index 0000000..b8a293f --- /dev/null +++ b/templates/function/node.html.mustache @@ -0,0 +1,114 @@ + + + + + diff --git a/templates/function/node.js.mustache b/templates/function/node.js.mustache new file mode 100644 index 0000000..2fcf6cd --- /dev/null +++ b/templates/function/node.js.mustache @@ -0,0 +1,256 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +module.exports = function(RED) { + "use strict"; + var util = require("util"); + var vm = require("vm"); + + function sendResults(node,_msgid,msgs) { + if (msgs == null) { + return; + } else if (!util.isArray(msgs)) { + msgs = [msgs]; + } + var msgCount = 0; + for (var m=0; m0) { + node.send(msgs); + } + } + + function FunctionNode(n) { + RED.nodes.createNode(this,n); + var node = this; + this.name = n.name; + this.func = n.func; + var functionText = "var results = null;"+ + "results = (function(msg){ "+ + "var __msgid__ = msg._msgid;"+ + "var node = {"+ + "log:__node__.log,"+ + "error:__node__.error,"+ + "warn:__node__.warn,"+ + "on:__node__.on,"+ + "status:__node__.status,"+ + "send:function(msgs){ __node__.send(__msgid__,msgs);}"+ + "};\n"+ + "{{&func}}"+"\n"+ + "})(msg);"; + this.topic = n.topic; + this.outstandingTimers = []; + this.outstandingIntervals = []; + var sandbox = { + console:console, + util:util, + Buffer:Buffer, + RED: { + util: RED.util + }, + __node__: { + log: function() { + node.log.apply(node, arguments); + }, + error: function() { + node.error.apply(node, arguments); + }, + warn: function() { + node.warn.apply(node, arguments); + }, + send: function(id, msgs) { + sendResults(node, id, msgs); + }, + on: function() { + if (arguments[0] === "input") { + throw new Error(RED._("function.error.inputListener")); + } + node.on.apply(node, arguments); + }, + status: function() { + node.status.apply(node, arguments); + } + }, + context: { + set: function() { + node.context().set.apply(node,arguments); + }, + get: function() { + return node.context().get.apply(node,arguments); + }, + keys: function() { + return node.context().keys.apply(node,arguments); + }, + get global() { + return node.context().global; + }, + get flow() { + return node.context().flow; + } + }, + flow: { + set: function() { + node.context().flow.set.apply(node,arguments); + }, + get: function() { + return node.context().flow.get.apply(node,arguments); + }, + keys: function() { + return node.context().flow.keys.apply(node,arguments); + } + }, + global: { + set: function() { + node.context().global.set.apply(node,arguments); + }, + get: function() { + return node.context().global.get.apply(node,arguments); + }, + keys: function() { + return node.context().global.keys.apply(node,arguments); + } + }, + setTimeout: function () { + var func = arguments[0]; + var timerId; + arguments[0] = function() { + sandbox.clearTimeout(timerId); + try { + func.apply(this,arguments); + } catch(err) { + node.error(err,{}); + } + }; + timerId = setTimeout.apply(this,arguments); + node.outstandingTimers.push(timerId); + return timerId; + }, + clearTimeout: function(id) { + clearTimeout(id); + var index = node.outstandingTimers.indexOf(id); + if (index > -1) { + node.outstandingTimers.splice(index,1); + } + }, + setInterval: function() { + var func = arguments[0]; + var timerId; + arguments[0] = function() { + try { + func.apply(this,arguments); + } catch(err) { + node.error(err,{}); + } + }; + timerId = setInterval.apply(this,arguments); + node.outstandingIntervals.push(timerId); + return timerId; + }, + clearInterval: function(id) { + clearInterval(id); + var index = node.outstandingIntervals.indexOf(id); + if (index > -1) { + node.outstandingIntervals.splice(index,1); + } + } + }; + if (util.hasOwnProperty('promisify')) { + sandbox.setTimeout[util.promisify.custom] = function(after, value) { + return new Promise(function(resolve, reject) { + sandbox.setTimeout(function(){ resolve(value) }, after); + }); + } + } + var context = vm.createContext(sandbox); + try { + this.script = vm.createScript(functionText); + this.on("input", function(msg) { + try { + var start = process.hrtime(); + context.msg = msg; + this.script.runInContext(context); + sendResults(this,msg._msgid,context.results); + + var duration = process.hrtime(start); + var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100; + this.metric("duration", msg, converted); + if (process.env.NODE_RED_FUNCTION_TIME) { + this.status({fill:"yellow",shape:"dot",text:""+converted}); + } + } catch(err) { + + var line = 0; + var errorMessage; + var stack = err.stack.split(/\r?\n/); + if (stack.length > 0) { + while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { + line++; + } + + if (line < stack.length) { + errorMessage = stack[line]; + var m = /:(\d+):(\d+)$/.exec(stack[line+1]); + if (m) { + var lineno = Number(m[1])-1; + var cha = m[2]; + errorMessage += " (line "+lineno+", col "+cha+")"; + } + } + } + if (!errorMessage) { + errorMessage = err.toString(); + } + this.error(errorMessage, msg); + } + }); + this.on("close", function() { + while (node.outstandingTimers.length > 0) { + clearTimeout(node.outstandingTimers.pop()) + } + while (node.outstandingIntervals.length > 0) { + clearInterval(node.outstandingIntervals.pop()) + } + this.status({}); + }) + } catch(err) { + // eg SyntaxError - which v8 doesn't include line number information + // so we can't do better than this + this.error(err); + } + } + RED.nodes.registerType("{{&nodeName}}", FunctionNode); + RED.library.register("functions"); +} diff --git a/templates/function/package.json.mustache b/templates/function/package.json.mustache new file mode 100644 index 0000000..14fbf16 --- /dev/null +++ b/templates/function/package.json.mustache @@ -0,0 +1,11 @@ +{ + "name": "{{&projectName}}", + "version": "{{&projectVersion}}", + "description": "Node-RED node for {{&nodeName}}", + "main": "node.js", + "node-red": { + "nodes": { + "{{&nodeName}}": "node.js" + } + } +} diff --git a/templates/swagger/LICENSE.mustache b/templates/swagger/LICENSE.mustache new file mode 100644 index 0000000..5b5498d --- /dev/null +++ b/templates/swagger/LICENSE.mustache @@ -0,0 +1,3 @@ +{{&licenseName}} +{{&licenseUrl}} + diff --git a/templates/swagger/README.md.mustache b/templates/swagger/README.md.mustache new file mode 100644 index 0000000..3ac6ded --- /dev/null +++ b/templates/swagger/README.md.mustache @@ -0,0 +1,26 @@ +{{&projectName}} +===================== + +Node-RED node for {{&nodeName}} + +{{&description}} + +Install +------- + +Run the following command in your Node-RED user directory - typically `~/.node-red` + + npm install {{&projectName}} + +Usage +----- + +### Methods + +{{#methods}} +- {{&methodName}} + + {{&summary}} + +{{/methods}} + diff --git a/templates/swagger/icons/icon.png b/templates/swagger/icons/icon.png new file mode 100644 index 0000000..b792bd8 Binary files /dev/null and b/templates/swagger/icons/icon.png differ diff --git a/templates/swagger/locales/en-US/node.json.mustache b/templates/swagger/locales/en-US/node.json.mustache new file mode 100644 index 0000000..d86cdc0 --- /dev/null +++ b/templates/swagger/locales/en-US/node.json.mustache @@ -0,0 +1,23 @@ +{ + "{{&className}}": { + "label": { + "service": "Service", + "method": "Method", + "host": "Host", + "header": "Header", + "value": "Value", + "isQuery": "isQuery" + }, + "status": { + "requesting": "requesting" + }, + "parameters": { + {{#methods}} + {{#parameters}} + "{{&camelCaseName}}": "{{&camelCaseName}}", + {{/parameters}} + {{/methods}} + "optionalParameters": "Option" + } + } +} diff --git a/templates/swagger/locales/ja/node.json.mustache b/templates/swagger/locales/ja/node.json.mustache new file mode 100644 index 0000000..b479ec1 --- /dev/null +++ b/templates/swagger/locales/ja/node.json.mustache @@ -0,0 +1,23 @@ +{ + "{{&className}}": { + "label": { + "service": "サービス", + "method": "メソッド", + "host": "ホスト", + "header": "ヘッダ", + "value": "値", + "isQuery": "クエリ" + }, + "status": { + "requesting": "要求中" + }, + "parameters": { + {{#methods}} + {{#parameters}} + "{{&camelCaseName}}": "{{&camelCaseName}}", + {{/parameters}} + {{/methods}} + "optionalParameters": "任意項目" + } + } +} diff --git a/templates/swagger/locales/zh-CN/node.json.mustache b/templates/swagger/locales/zh-CN/node.json.mustache new file mode 100644 index 0000000..d86cdc0 --- /dev/null +++ b/templates/swagger/locales/zh-CN/node.json.mustache @@ -0,0 +1,23 @@ +{ + "{{&className}}": { + "label": { + "service": "Service", + "method": "Method", + "host": "Host", + "header": "Header", + "value": "Value", + "isQuery": "isQuery" + }, + "status": { + "requesting": "requesting" + }, + "parameters": { + {{#methods}} + {{#parameters}} + "{{&camelCaseName}}": "{{&camelCaseName}}", + {{/parameters}} + {{/methods}} + "optionalParameters": "Option" + } + } +} diff --git a/templates/swagger/node.html.mustache b/templates/swagger/node.html.mustache new file mode 100644 index 0000000..8a623cc --- /dev/null +++ b/templates/swagger/node.html.mustache @@ -0,0 +1,304 @@ + + + + + + + + + + + + diff --git a/templates/swagger/node.js.mustache b/templates/swagger/node.js.mustache new file mode 100644 index 0000000..6baee06 --- /dev/null +++ b/templates/swagger/node.js.mustache @@ -0,0 +1,137 @@ +"use strict"; +var lib = require('./lib.js'); + +module.exports = function (RED) { + function {{&className}}Node(config) { + RED.nodes.createNode(this, config); + this.service = RED.nodes.getNode(config.service); + this.method = config.method; + + {{#methods}} + {{#parameters}} + this.{{&methodName}}_{{&camelCaseName}} = config.{{&methodName}}_{{&camelCaseName}}; + {{/parameters}} + {{/methods}} + + var node = this; + + node.on('input', function (msg) { + {{#domain}} + var client = new lib.{{&className}}(); + {{/domain}} + {{^domain}} + var client = new lib.{{&className}}({ domain: this.service.host }); + {{/domain}} + + {{#isSecure}} + {{#isSecureToken}} + if (this.service.secureTokenIsQuery) { + client.setApiKey(this.service.credentials.secureTokenValue, + this.service.secureTokenHeaderOrQueryName, true); + } else { + client.setApiKey(this.service.credentials.secureTokenValue, + this.service.secureTokenHeaderOrQueryName, false); + } + {{/isSecureToken}} + + {{#isSecureApiKey}} + if (this.service.secureApiKeyIsQuery) { + client.setApiKey(this.service.credentials.secureApiKeyValue, + this.service.secureApiKeyHeaderOrQueryName, true); + } else { + client.setApiKey(this.service.credentials.secureApiKeyValue, + this.service.secureApiKeyHeaderOrQueryName, false); + } + {{/isSecureApiKey}} + + {{#isSecureBasic}} + client.setBasicAuth(this.service.credentials.username, this.service.credentials.password); + {{/isSecureBasic}} + {{/isSecure}} + + client.body = msg.payload; + + var result; + var errorFlag = false; + {{#methods}} + if (node.method === '{{&methodName}}') { + var parameters = []; + {{#parameters}} + if ('{{&camelCaseName}}' === 'body') { + if (typeof msg.payload === 'object') { + parameters.{{&camelCaseName}} = msg.payload; + } else { + node.error('Unsupported type: \'' + (typeof msg.payload) + '\', ' + + 'msg.payload must be JSON object or buffer.'); + errorFlag = true; + } + } else { + parameters.{{&camelCaseName}} = node.{{&methodName}}_{{&camelCaseName}}; + } + {{/parameters}} + result = client.{{&methodName}}(parameters); + } + {{/methods}} + if (!errorFlag) { + node.status({ fill: "blue", shape: "dot", text: "{{&className}}.status.requesting" }); + result.then(function (response) { + if (response.body !== null) { + msg.payload = response.body; + } + node.send(msg); + node.status({}); + }).catch(function (error) { + node.error(error); + node.status({ fill: "red", shape: "ring", text: "node-red:common.status.error" }); + }); + } + }); + } + RED.nodes.registerType("{{&nodeName}}", {{&className}}Node); + + function {{&className}}ServiceNode(n) { + RED.nodes.createNode(this, n); + {{^domain}} + this.host = n.host; + {{/domain}} + + {{#isSecure}} + {{#isSecureToken}} + this.secureTokenValue = n.secureTokenValue; + this.secureTokenHeaderOrQueryName = n.secureTokenHeaderOrQueryName; + this.secureTokenIsQuery = n.secureTokenIsQuery; + {{/isSecureToken}} + + {{#isSecureApiKey}} + this.secureApiKeyValue = n.secureApiKeyValue; + this.secureApiKeyHeaderOrQueryName = n.secureApiKeyHeaderOrQueryName; + this.secureApiKeyIsQuery = n.secureApiKeyIsQuery; + {{/isSecureApiKey}} + + {{#isSecureBasic}} + this.username = n.username; + this.password = n.password; + {{/isSecureBasic}} + {{/isSecure}} + } + RED.nodes.registerType("{{&nodeName}}-service", {{&className}}ServiceNode, { + credentials: { + {{#isSecure}} + {{#isSecureToken}} + secureTokenValue: { type: "password" }, + {{/isSecureToken}} + + {{#isSecureApiKey}} + secureApiKeyValue: { type: "password" }, + {{/isSecureApiKey}} + + {{#isSecureBasic}} + username: { type: "text" }, + password: { type: "password" }, + {{/isSecureBasic}} + {{/isSecure}} + temp: { type: "text" } + } + }); +}; + diff --git a/templates/swagger/package.json.mustache b/templates/swagger/package.json.mustache new file mode 100644 index 0000000..369c869 --- /dev/null +++ b/templates/swagger/package.json.mustache @@ -0,0 +1,17 @@ +{ + "name": "{{&projectName}}", + "version": "{{&projectVersion}}", + "description": "Node-RED node for {{&nodeName}}", + "main": "node.js", + "node-red": { + "nodes": { + "{{&nodeName}}": "node.js" + } + }, + "dependencies": { + "q": "1.5.1", + "request": "2.83.0" + }, + "author": "{{&contactName}}", + "license": "{{&licenseName}}" +}