├── .gitignore ├── .hygen.js ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── _templates └── gen │ ├── generateGenericFunctions │ └── GenericFunctions.ejs.gen │ ├── generateKeyCredential │ └── KeyCredential.ejs.gen │ ├── generateNodeCredentialDocs │ └── NodeCredentialDocs.ejs.gen │ ├── generateNodeMainDocs │ └── NodeMainDocs.ejs.gen │ ├── generateOAuth2Credential │ └── OAuth2Credential.ejs.gen │ ├── generateRegularNodeComplex │ └── RegularNodeComplex.ejs.gen │ ├── generateRegularNodeSimple │ └── RegularNodeSimple.ejs.gen │ ├── generateResourceDescription │ └── ResourceDescription.ejs.gen │ ├── generateTriggerNodeSimple │ └── TriggerNodeSimple.ejs.gen │ ├── updateCredentialPackageJson │ └── packageJsonCredential.ejs.inj │ └── updateNodePackageJson │ └── packageJsonNode.ejs.inj ├── build └── .gitkeep ├── client ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── Requester.ts ├── assests │ └── n8n.png ├── babel.config.js ├── channels │ ├── DocsgenChannel.ts │ ├── EmptyChannel.ts │ ├── ExampleChannel.ts │ ├── IpcChannel.interface.ts │ ├── NodegenChannel.ts │ ├── PackgenChannel.ts │ └── PlacementChannel.ts ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── n8n.png │ │ └── n8n_copy.png │ ├── background.ts │ ├── components │ │ ├── AddButton.vue │ │ ├── BackwardButton.vue │ │ ├── Checkbox.vue │ │ ├── Dropdown.vue │ │ ├── ForwardButton.vue │ │ ├── GenericButton.vue │ │ ├── Header.vue │ │ ├── InputField.vue │ │ ├── Instructions.vue │ │ ├── SmallButton.vue │ │ ├── Switch.vue │ │ └── TextArea.vue │ ├── main.ts │ ├── mixins │ │ ├── params-build-mixin.ts │ │ └── routes-mixin.ts │ ├── router │ │ └── index.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ ├── store │ │ ├── index.ts │ │ ├── modules │ │ │ ├── basicInfo.ts │ │ │ ├── docsInfo.ts │ │ │ ├── fields.ts │ │ │ ├── operations.ts │ │ │ ├── properties.ts │ │ │ └── resources.ts │ │ └── types.ts │ └── views │ │ ├── BasicInfo.vue │ │ ├── RegularNode │ │ ├── Complete.vue │ │ ├── Fields.vue │ │ ├── Operations.vue │ │ └── Resources.vue │ │ └── TriggerNode │ │ ├── Complete.vue │ │ ├── Fields.vue │ │ └── Properties.vue └── tsconfig.json ├── config └── index.ts ├── docs ├── README.md ├── cli-reference.md ├── codebase-functionality.md ├── images │ ├── graphs │ │ ├── backend-frontend-communication.png │ │ ├── backend.svg │ │ └── frontend.svg │ ├── icons │ │ ├── icons8-bug-64.png │ │ ├── icons8-code-file-80.png │ │ ├── icons8-console-64.png │ │ ├── icons8-product-documents-64.png │ │ └── icons8-repository-80.png │ ├── logos │ │ ├── electron.png │ │ ├── node.png │ │ ├── ts.png │ │ └── vue.png │ ├── nodemaker-banner.png │ └── screencaps │ │ ├── credentials.png │ │ ├── icongen-output.png │ │ ├── icongen-querystring.png │ │ ├── node-doc.png │ │ ├── node.png │ │ ├── nodegen-prompt.png │ │ ├── packageJson.png │ │ ├── place-prompt.png │ │ ├── place-small.png │ │ ├── placement.png │ │ ├── resize-icon-candidates.png │ │ ├── resize-prompt.png │ │ ├── typeguard-error.png │ │ ├── validation-error.png │ │ ├── workflow-submission.png │ │ └── workflow.png ├── issue-submission-guidelines.md ├── official-repos-setup.md └── output-examples │ ├── GenericFunctions.ts │ ├── HackerNews.md │ ├── HackerNews.node.ts │ ├── HackerNewsCredentials.md │ └── HackerNewsOAuth2Api.credentials.ts ├── generators ├── Generator.ts ├── NodeDocsGenerator.ts ├── NodeFilesGenerator.ts └── PackageJsonGenerator.ts ├── globals.d.ts ├── output └── .gitkeep ├── package-lock.json ├── package.json ├── parameters.ts ├── scripts ├── createWorkflow.ts ├── emptyOutputDir.ts ├── generateIconCandidates.ts ├── generateNodeDocs.ts ├── generateNodeFiles.ts ├── generatePackageJson.ts ├── placeFiles.ts ├── resizeIcon.ts ├── takeScreenshot.ts └── validateParams.ts ├── services ├── DirectoryEmptier.ts ├── FileFinder.ts ├── FilePlacer.ts ├── Highlighter.ts ├── IconResizer.ts ├── ImageFetcher.ts ├── Prompter.ts ├── ScreenshotTaker.ts ├── Validator.ts └── WorkflowCreator.ts ├── tester.json ├── tsconfig.json └── utils ├── constants.ts ├── enums.ts ├── getWorkflowSubmissionUrl.ts ├── sleep.ts └── typeGuards.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /output/* 3 | /build/* 4 | !build/.gitkeep 5 | /config/.env -------------------------------------------------------------------------------- /.hygen.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | helpers: { 3 | /**Check if a field or option is collective (contains fields or options).*/ 4 | isCollective: (entity) => 5 | entity.type === "collection" || 6 | entity.type === "fixedCollection" || 7 | entity.type === "multiOptions" || 8 | entity.type === "options", 9 | /**Format a string as a class name, uppercase for each initial and no whitespace.*/ 10 | classify: (name) => name.replace(/\s/g, ""), 11 | /**Format a string as a lowercase single word, or a lowercase first word and uppercase initial + lowercase rest for following words.*/ 12 | camelify: (input) => { 13 | const isSingleWord = input.split(" ").length === 1; 14 | const uppercaseInitialLowercaseRest = (input) => 15 | input[0].toUpperCase() + input.slice(1).toLowerCase(); 16 | 17 | if (isSingleWord) return input.toLowerCase(); 18 | 19 | // multi-word 20 | let result = []; 21 | input.split(" ").forEach((word, index) => { 22 | index === 0 23 | ? result.push(word.toLowerCase()) 24 | : result.push(uppercaseInitialLowercaseRest(word)); 25 | }); 26 | return result.join(""); 27 | }, 28 | /**Check if operations array has a Get All operation.*/ 29 | hasGetAll: (operations) => 30 | operations.some((operation) => operation.name === "Get All"), 31 | /**Check if a particular operation is a GET request.*/ 32 | isRetrieval: (operation) => operation.requestMethod === "GET", 33 | /**Check if a particular operation has any associated additional fields.*/ 34 | hasAdditionalFields: (operation) => 35 | operation.fields.filter((field) => field.name === "Additional Fields"), 36 | /**Create the credential string for based on auth type.*/ 37 | getCredentialsString: (name, auth) => 38 | name + (auth === "OAuth2" ? "OAuth2" : "") + "Api", 39 | /**Check if the endpoint has an extractable variable.*/ 40 | hasEndpointVariable: (endpoint) => endpoint.split("").includes(":"), 41 | /**Extract the variable from the endpoint.*/ 42 | getVariableFromEndpoint: (endpoint) => endpoint.match(/:(.*)(\/)?/)[1], 43 | /**Reformat endpoint from colon version to string-interpolated version.*/ 44 | fixEndpoint: (endpoint) => { 45 | const routeParameter = endpoint.match(/:(.*)(\/)?/)[1]; 46 | return endpoint.replace( 47 | ":" + routeParameter, 48 | "${" + routeParameter + "}" 49 | ); 50 | }, 51 | 52 | hasNumericalLimits: (field) => field.numericalLimits, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[typescript]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "[markdown]": { 9 | "editor.formatOnSave": true 10 | }, 11 | "[vue]": { 12 | "editor.formatOnSave": true 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | © 2020 Iván Ovejero and Erin McNulty 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nodemaker 3 |

4 | 5 |

6 |

Nodemaker

7 |

8 | 9 |

10 | Desktop app and CLI utility to auto-generate n8n nodes
11 | by Iván Ovejero and Erin McNulty 12 |

13 | 14 |

15 | Installation • 16 | Operation • 17 | Examples • 18 | Documentation 19 |

20 | 21 |

22 | 23 | 24 | 25 |

26 | 27 |
28 | 29 | **Nodemaker** is an automatic node generator for [n8n](https://github.com/n8n-io/n8n), a workflow automation tool. Nodemaker outputs all functionality and documentation files for a node, places them in the official repos, and uploads a sample workflow to [n8n.io](https://n8n.io/workflows). 30 | 31 | Developed as a **desktop app** and **CLI utility**, in MVP stage, as a capstone project for the [MLH Fellowship](https://github.com/MLH-Fellowship). 32 | 33 |
34 | 35 |

36 | TypeScript logo 37 |      38 | Node.js logo 39 |      40 | Electron logo 41 |      42 | Vue.js logo 43 |

44 | 45 |

46 | Built with TypeScript, Node, Electron and Vue 47 |

48 | 49 | ## Installation 50 | 51 | To set up Nodemaker: 52 | 53 | ```sh 54 | git clone https://github.com/MLH-Fellowship/nodemaker.git 55 | cd nodemaker && npm run setup 56 | ``` 57 | 58 | To quickly see it working: 59 | 60 | ```sh 61 | npm run nodegen 62 | ``` 63 | 64 | With `nodegen`, Nodemaker will read the built-in sample params and generate sample output files in `/output`. 65 | 66 | ## Operation 67 | 68 | Nodemaker can be operated as a desktop app or as a CLI utility. 69 | 70 | - To run the desktop app: `npm run desktop` 71 | - To run the CLI, see the [CLI reference](/docs/cli-reference.md). 72 | 73 | Nodemaker generates two types of files: 74 | 75 | **Node functionality files** 76 | 77 | - `*.node.ts` — main logic (regular or trigger node) 78 | - `GenericFunctions.ts` — node helper functions 79 | - `*Description.ts` — separate logic per resource (optional) 80 | - `*.credentials.ts` — node authentication params 81 | - `package.json` — updated node listing 82 | - `*.png` — node icon 83 | 84 | | ![](docs/images/screencaps/node.png) ![](docs/images/screencaps/credentials.png) | 85 | | :------------------------------------------------------------------------------: | 86 | | Auto-generated `HackerNews.node.ts` and `GenericFunctions.ts` | 87 | 88 |
89 | 90 | **Node documentation files** 91 | 92 | - `README.md` — main doc file 93 | - `README.md` — credentials doc file 94 | - `workflow.png` — in-app screenshot for main doc file 95 | 96 | | ![](docs/images/screencaps/node-doc.png) ![](docs/images/screencaps/workflow.png) | 97 | | :-------------------------------------------------------------------------------: | 98 | | Auto-generated `README.md` and `workflow.png` | 99 | 100 |
101 | 102 | Once these files are generated, Nodemaker can: 103 | 104 | - relocate them in your copies of the [n8n](https://github.com/n8n-io/n8n) and [n8n-docs](https://github.com/n8n-io/n8n-docs) repos, and 105 | - submit a sample workflow to the [n8n.io](https://n8n.io/workflows) collection. 106 | 107 | | ![](docs/images/screencaps/placement.png) | 108 | | :---------------------------------------------------------------------------------------------------------------: | 109 | | Node files auto-placed in `n8n` repo and workflow auto-submitted on [n8n.io](https://n8n.io/workflows) | 110 | 111 | ## Authors 112 | 113 | © 2020 [Iván Ovejero](https://github.com/ivov) and [Erin McNulty](https://github.com/erin2722) 114 | 115 | ## License 116 | 117 | Distributed under the MIT License. See [LICENSE.md](LICENSE.md). 118 | -------------------------------------------------------------------------------- /_templates/gen/generateGenericFunctions/GenericFunctions.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/GenericFunctions.ts 3 | mainParameters: <%= mainParameters %> 4 | metaParameters: <%= metaParameters %> 5 | --- 6 | <%_ 7 | mainParameters = JSON.parse(mainParameters); 8 | metaParameters = JSON.parse(metaParameters); 9 | _%> 10 | import { 11 | IExecuteFunctions, 12 | IHookFunctions, 13 | } from 'n8n-core'; 14 | 15 | import { 16 | IDataObject, 17 | } from 'n8n-workflow'; 18 | 19 | import { 20 | OptionsWithUri, 21 | } from 'request'; 22 | 23 | 24 | /** 25 | * Make an API request to <%= metaParameters.serviceName %> 26 | * 27 | * @param {IHookFunctions | IExecuteFunctions} this 28 | * @param {string} method 29 | * @param {string} endpoint 30 | * @param {IDataObject} body 31 | * @param {IDataObject} qs 32 | * @param {string} [uri] 33 | * @param {IDataObject} [headers] 34 | * @returns {Promise} 35 | */ 36 | export async function <%= h.camelify(metaParameters.serviceName) %>ApiRequest( 37 | this: IHookFunctions | IExecuteFunctions, 38 | method: string, 39 | endpoint: string, 40 | body: IDataObject, 41 | qs: IDataObject, 42 | uri?: string, 43 | headers?: IDataObject, 44 | ): Promise { // tslint:disable-line:no-any 45 | 46 | const options: OptionsWithUri = { 47 | headers: {}, 48 | body, 49 | method, 50 | qs, 51 | uri: uri || `<%= metaParameters.apiUrl %>${endpoint}`, 52 | json: true, 53 | }; 54 | 55 | try { 56 | <%_ if (metaParameters.authType !== "None") { _%> 57 | 58 | const credentials = this.getCredentials('<%= h.getCredentialsString(h.camelify(metaParameters.serviceName), metaParameters.authType) %>'); 59 | 60 | if (credentials === undefined) { 61 | throw new Error('No credentials got returned!'); 62 | } 63 | <%_ } _%> 64 | 65 | if (Object.keys(headers).length !== 0) { 66 | options.headers = Object.assign({}, options.headers, headers); 67 | } 68 | 69 | if (Object.keys(body).length === 0) { 70 | delete options.body; 71 | } 72 | 73 | <%_ if (metaParameters.authType === "OAuth2") { _%> 74 | return await this.helpers.requestOAuth2.call(this, '<%= h.getCredentialsString(h.camelify(metaParameters.serviceName), metaParameters.auth) %>', options); 75 | <%_ } else { _%> 76 | return await this.helpers.request!(options); 77 | <%_ } _%> 78 | 79 | } catch (error) { 80 | 81 | // TODO: Replace TODO_ERROR_STATUS_CODE and TODO_ERROR_MESSAGE based on the error object returned by API. 82 | 83 | <%_ if (metaParameters.authType !== "None") { _%> 84 | if (TODO_ERROR_STATUS_CODE === 401) { 85 | // Return a clear error 86 | throw new Error('The <%= metaParameters.serviceName %> credentials are invalid!'); 87 | } 88 | 89 | <%_ } _%> 90 | if (TODO_ERROR_MESSAGE) { 91 | // Try to return the error prettier 92 | throw new Error(`<%= metaParameters.serviceName %> error response [${TODO_ERROR_STATUS_CODE}]: ${TODO_ERROR_MESSAGE}`); 93 | } 94 | 95 | // If that data does not exist for some reason, return the actual error. 96 | throw error; 97 | } 98 | } 99 | 100 | 101 | /** 102 | * Make an API request to <%= metaParameters.serviceName %> and return all results 103 | * 104 | * @export 105 | * @param {IHookFunctions | IExecuteFunctions} this 106 | * @param {string} method 107 | * @param {string} endpoint 108 | * @param {IDataObject} body 109 | * @param {IDataObject} qs 110 | * @returns {Promise} 111 | */ 112 | export async function <%= h.camelify(metaParameters.serviceName) %>ApiRequestAllItems( 113 | this: IHookFunctions | IExecuteFunctions, 114 | propertyName: string, 115 | method: string, 116 | endpoint: string, 117 | body: IDataObject, 118 | qs: IDataObject 119 | ): Promise { // tslint:disable-line:no-any 120 | 121 | const returnData: IDataObject[] = []; 122 | let responseData: any; 123 | 124 | do { 125 | responseData = await <%= h.camelify(metaParameters.serviceName) %>ApiRequest.call(this, method, endpoint, body, qs); 126 | // TODO: Get next page using `responseData` or `qs` 127 | returnData.push.apply(returnData, responseData[propertyName]); 128 | 129 | } while ( 130 | // TODO: Add condition for total not yet reached 131 | ); 132 | 133 | return returnData; 134 | } 135 | -------------------------------------------------------------------------------- /_templates/gen/generateKeyCredential/KeyCredential.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/<%= serviceCredential %>.credentials.ts 3 | serviceCredential: <%= serviceCredential %> 4 | --- 5 | import { 6 | ICredentialType, 7 | NodePropertyTypes, 8 | } from 'n8n-workflow'; 9 | 10 | 11 | export class <%= serviceCredential %> implements ICredentialType { 12 | name = '<%= serviceCredential %>'; 13 | extends = [ 14 | 'oAuth2Api' 15 | ]; 16 | displayName = '<%= name %> OAuth2 API'; 17 | properties = [ 18 | { 19 | displayName: 'API Key', 20 | // name: '...', // TODO: Fill in key parameter name per API 21 | type: 'string' as NodePropertyTypes, 22 | default: '', 23 | } 24 | ]; 25 | } -------------------------------------------------------------------------------- /_templates/gen/generateNodeCredentialDocs/NodeCredentialDocs.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/<%= name.replace(/ /g, "") %>Credentials.md 3 | docsParameters: <%= docsParameters %> 4 | metaParameters: <%= metaParameters %> 5 | --- 6 | <%_ 7 | docsParameters = JSON.parse(docsParameters); 8 | metaParameters = JSON.parse(metaParameters); 9 | filename = name.replace(/ /g, "") 10 | _%> 11 | --- 12 | permalink: /nodes/n8n-nodes-base.<%= h.camelify(docsParameters.serviceName) %> 13 | --- 14 | 15 | # <%= docsParameters.serviceName %> 16 | 17 | You can find information about the operations supported by the <%= docsParameters.serviceName %> node on the [integrations](https://n8n.io/integrations/n8n-nodes-base.<%= h.camelify(docsParameters.serviceName) %>) page. You can also browse the source code of the node on [GitHub](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes/<%= docsParameters.serviceName.replace(/ /g, "") %>). 18 | 19 | ## Pre-requisites 20 | 21 | Create a [<%= name %>](<%= docsParameters.serviceUrl %>) account. 22 | 23 | <%_ if (metaParameters.authType === "API Key") { _%> 24 | 25 | ## Using Access Token 26 | 27 | 28 | <%_ } else if (metaParameters.authType === "OAuth2") { _%> 29 | ## Using OAuth 30 | 31 | 32 | <%_ } _%> -------------------------------------------------------------------------------- /_templates/gen/generateNodeMainDocs/NodeMainDocs.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/<%= name.replace(/ /g, "") %>.md 3 | docsParameters: <%= docsParameters %> 4 | nodeOperations: <%= nodeOperations %> 5 | --- 6 | <%_ 7 | docsParameters = JSON.parse(docsParameters); 8 | nodeOperations = JSON.parse(nodeOperations); 9 | filename = name.replace(/ /g, "") 10 | _%> 11 | --- 12 | permalink: /nodes/n8n-nodes-base.<%= h.camelify(docsParameters.serviceName) %> 13 | --- 14 | 15 | # <%= docsParameters.serviceName %> 16 | 17 | [<%= docsParameters.serviceName %>](<%= docsParameters.serviceUrl %>) is <%= docsParameters.introDescription %>. 18 | 19 | ::: tip 🔑 Credentials 20 | You can find authentication information for this node [here](../../../credentials/<%= filename %>/README.md). 21 | ::: 22 | 23 | ## Basic Operations 24 | 25 | <%_ Object.keys(nodeOperations).forEach((resource) => { _%> 26 | - <%= resource %> 27 | <%_ for (let operation of nodeOperations[resource]) { _%> 28 | - <%= operation %> 29 | <%_ } _%> 30 | <%_ }); _%> 31 | 32 | ## Example Usage 33 | 34 | This workflow allows you to <%= docsParameters.exampleUsage %>. You can also find the [workflow](<%= docsParameters.workflowUrl %>) on this website. This example usage workflow would use the following two nodes. 35 | - [Start](../../core-nodes/Start/README.md) 36 | - [<%= docsParameters.serviceName %>]() 37 | 38 | The final workflow should look like the following image. 39 | 40 | ![A workflow with the <%= docsParameters.serviceName %> node](./workflow.png) 41 | 42 | ### 1. Start node 43 | 44 | The start node exists by default when you create a new workflow. 45 | 46 | ### 2. <%= docsParameters.serviceName %> node 47 | 48 | 1. First of all, you'll have to enter credentials for the <%= docsParameters.serviceName %> node. You can find out how to do that [here](../../../credentials/<%= docsParameters.serviceName %>/README.md). 49 | 50 | X. Click on *Execute Node* to run the workflow. 51 | 52 | -------------------------------------------------------------------------------- /_templates/gen/generateOAuth2Credential/OAuth2Credential.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/<%= serviceCredential %>.credentials.ts 3 | serviceCredential: <%= serviceCredential %> 4 | --- 5 | import { 6 | ICredentialType, 7 | NodePropertyTypes, 8 | } from 'n8n-workflow'; 9 | 10 | 11 | export class <%= serviceCredential %> implements ICredentialType { 12 | name = '<%= serviceCredential %>'; 13 | extends = [ 14 | 'oAuth2Api' 15 | ]; 16 | displayName = '<%= name %> OAuth2 API'; 17 | properties = [ 18 | { 19 | displayName: 'Authorization URL', 20 | name: 'authUrl', 21 | type: 'hidden' as NodePropertyTypes, 22 | // default: '...', // TODO: Fill in authorization URL 23 | }, 24 | { 25 | displayName: 'Access Token URL', 26 | name: 'accessTokenUrl', 27 | type: 'hidden' as NodePropertyTypes, 28 | // default: '...', // TODO: Fill in access token URL 29 | }, 30 | { 31 | displayName: 'Scope', 32 | name: 'scope', 33 | type: 'hidden' as NodePropertyTypes, 34 | // default: '...', // TODO: Fill in default scopes 35 | }, 36 | { 37 | displayName: 'Authentication', 38 | name: 'authentication', 39 | type: 'hidden' as NodePropertyTypes, 40 | // default: '...', // TODO: Select 'header' or 'body' 41 | }, 42 | // TODO: Select auth query parameters if necessary 43 | // { 44 | // displayName: 'Auth URI Query Parameters', 45 | // name: 'authQueryParameters', 46 | // type: 'hidden' as NodePropertyTypes, 47 | // default: 'response_type=code', 48 | // }, 49 | // { 50 | // displayName: 'Auth URI Query Parameters', 51 | // name: 'authQueryParameters', 52 | // type: 'hidden' as NodePropertyTypes, 53 | // default: 'grant_type=authorization_code', 54 | // }, 55 | ]; 56 | } -------------------------------------------------------------------------------- /_templates/gen/generateRegularNodeComplex/RegularNodeComplex.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/<%= name.replace(/\s/g, "") %>.node.ts 3 | mainParameters: <%= mainParameters %> 4 | metaParameters: <%= metaParameters %> 5 | --- 6 | <%_ 7 | mainParameters = JSON.parse(mainParameters); 8 | metaParameters = JSON.parse(metaParameters); 9 | [operations] = Object.values(mainParameters); 10 | _%> 11 | import { 12 | IExecuteFunctions, 13 | } from 'n8n-core'; 14 | 15 | import { 16 | INodeExecutionData, 17 | INodeType, 18 | INodeTypeDescription, 19 | IDataObject, 20 | } from 'n8n-workflow'; 21 | 22 | import { 23 | <%= h.camelify(metaParameters.serviceName) %>ApiRequest, 24 | <%_ if (h.hasGetAll(operations)) { _%> 25 | <%= h.camelify(name) %>ApiRequestAllItems, 26 | <%_ } _%> 27 | } from './GenericFunctions'; 28 | 29 | <%_ Object.keys(mainParameters).forEach((resource) => { _%> 30 | import { 31 | <%= h.camelify(resource) %>Operations, 32 | <%= h.camelify(resource) %>Fields, 33 | } from './<%= h.camelify(resource) %>Description'; 34 | 35 | <%_ }) _%> 36 | 37 | export class <%= h.classify(metaParameters.serviceName) %> implements INodeType { 38 | description: INodeTypeDescription = { 39 | displayName: '<%= metaParameters.serviceName %>', 40 | name: '<%= h.camelify(metaParameters.serviceName) %>', 41 | icon: 'file:<%= h.camelify(metaParameters.serviceName) %>.png', 42 | group: ['transform'], 43 | version: 1, 44 | subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', 45 | description: 'Consume <%= metaParameters.serviceName %> API', 46 | defaults: { 47 | name: '<%= metaParameters.serviceName %>', 48 | color: '<%= metaParameters.nodeColor %>', 49 | }, 50 | inputs: ['main'], 51 | outputs: ['main'], 52 | properties: [ 53 | // ---------------------------------- 54 | // Resources 55 | // ---------------------------------- 56 | { 57 | displayName: 'Resource', 58 | name: 'resource', 59 | type: 'options', 60 | options: [ 61 | <%_ for (let resource of Object.keys(mainParameters)) { _%> 62 | { 63 | name: '<%= resource %>', 64 | value: '<%= h.camelify(resource) %>', 65 | }, 66 | <%_ } _%> 67 | ], 68 | default: '<%= Object.keys(mainParameters)[0].toLowerCase() %>', 69 | description: 'Resource to consume', 70 | }, 71 | // ---------------------------------- 72 | // Operations 73 | // ---------------------------------- 74 | <%_ Object.keys(mainParameters).forEach((resource) => { _%> 75 | ...<%= h.camelify(resource) %>Operations, 76 | ...<%= h.camelify(resource) %>Fields, 77 | <%_ }) _%> 78 | ], 79 | }; 80 | 81 | async execute(this: IExecuteFunctions): Promise { 82 | const items = this.getInputData(); 83 | const returnData: IDataObject[] = []; 84 | 85 | const resource = this.getNodeParameter('resource', 0) as string; 86 | const operation = this.getNodeParameter('operation', 0) as string; 87 | 88 | let responseData: any; 89 | 90 | for (let i = 0; i < items.length; i++) { 91 | 92 | let qs: IDataObject = {}; 93 | let body: IDataObject = {}; 94 | 95 | <%_ Object.keys(mainParameters).forEach(function(resource) { _%> 96 | <%_ if (Object.keys(mainParameters).indexOf(resource) !== 0) { _%> 97 | } else if (resource === '<%= h.camelify(resource) %>') { 98 | 99 | <%_ } else { _%> 100 | if (resource === '<%= h.camelify(resource) %>') { 101 | 102 | <%_ } _%> 103 | <%_ for (let operation of mainParameters[resource]) { _%> 104 | <%_ if (mainParameters[resource].indexOf(operation) !== 0) { _%> 105 | 106 | } else if (operation === '<%= h.camelify(operation.name) %>') { 107 | <%_ } else { _%> 108 | if (operation === '<%= h.camelify(operation.name) %>') { 109 | <%_ } _%> 110 | 111 | <%_ if (h.hasEndpointVariable(operation.endpoint)) { _%> 112 | const <%= h.getVariableFromEndpoint(operation.endpoint) %> = this.getNodeParameter('<%= h.getVariableFromEndpoint(operation.endpoint) %>', i); 113 | <%_ } _%> 114 | const endpoint =<%_ if (h.hasEndpointVariable(operation.endpoint)) { _%> 115 | <%_ %> `<%= h.fixEndpoint(operation.endpoint) _%>`; 116 | <%_ } else { _%> 117 | <%_ %> '<%= operation.endpoint _%>'; 118 | <%_ } _%> 119 | 120 | <%_ if (h.hasAdditionalFields(operation)) { _%> 121 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 122 | // TODO: Use additionalFields.fieldName in `qs` or in `body` or as boolean flag 123 | <%_ } _%> 124 | 125 | <%_ if (operation.name === "Get All") { _%> 126 | // TODO: Replace TODO_PROPERTY_NAME with the name of the property whose value is all the items 127 | responseData = await <%= h.camelify(metaParameters.serviceName) %>ApiRequestAllItems.call(this, TODO_PROPERTY_NAME, '<%= operation.requestMethod %>', endpoint, body, qs); 128 | <%_ } else if (h.isRetrieval(operation)) { _%> 129 | responseData = await <%= h.camelify(metaParameters.serviceName) %>ApiRequest.call(this, '<%= operation.requestMethod %>', endpoint, body, qs); 130 | <%_ } else { _%> 131 | await <%= h.camelify(metaParameters.serviceName) %>ApiRequest.call(this, '<%= operation.requestMethod %>', endpoint, body, qs); 132 | responseData = { success: true }; 133 | <%_ } _%> 134 | <%_ if (mainParameters[resource].indexOf(operation) + 1 === mainParameters[resource].length) { _%> 135 | 136 | } else { 137 | throw new Error(`The operation ${operation} is not known!`); 138 | } 139 | <%_ } else { _%> 140 | <%_ } _%> 141 | <%_ } _%> 142 | <%_ if (Object.keys(mainParameters).indexOf(resource) + 1 === Object.keys(mainParameters).length) { _%> 143 | 144 | } else { 145 | throw new Error(`The resource ${resource} is not known!`); 146 | } 147 | <%_ } _%> 148 | 149 | <%_ }); _%> 150 | if (Array.isArray(responseData)) { 151 | returnData.push.apply(returnData, responseData as IDataObject[]); 152 | } else { 153 | returnData.push(responseData as IDataObject); 154 | } 155 | 156 | } 157 | 158 | return [this.helpers.returnJsonArray(returnData)]; 159 | } 160 | } 161 | 162 | 163 | -------------------------------------------------------------------------------- /_templates/gen/generateResourceDescription/ResourceDescription.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/<%= resourceName.replace(/\s/g, "") %>Description.ts 3 | resourceObject: <%= resourceObject %> 4 | --- 5 | <%_ resourceObject = JSON.parse(resourceObject); _%> 6 | import { 7 | INodeProperties, 8 | } from 'n8n-workflow'; 9 | 10 | export const <%= h.camelify(resourceName) %>Operations = [ 11 | { 12 | displayName: 'Operation', 13 | name: 'operation', 14 | type: 'options', 15 | displayOptions: { 16 | show: { 17 | resource: [ 18 | '<%= h.camelify(resourceName) %>', 19 | ], 20 | }, 21 | }, 22 | options: [ 23 | <%_ for (let operation of resourceObject) { _%> 24 | { 25 | name: '<%= operation.name %>', 26 | value: '<%= h.camelify(operation.name) %>', 27 | description: '<%= operation.description %>', 28 | }, 29 | <%_ } _%> 30 | ], 31 | default: '<%= h.camelify(resourceObject[0].name) %>', 32 | description: 'Operation to perform', 33 | }, 34 | ] as INodeProperties[]; 35 | 36 | export const <%= h.camelify(resourceName) %>Fields = [ 37 | <%_ for (let operation of resourceObject) { _%> 38 | <%_ for (let field of operation.fields) { _%> 39 | { 40 | displayName: '<%= field.name %>', 41 | name: '<%= field.name %>', 42 | <%_ if (field.name !== 'Additional Fields') { _%> 43 | description: '<%= field.description %>', 44 | <%_ } _%> 45 | type: '<%= field.type %>', 46 | <%_ if (h.hasNumericalLimits(field)) { _%> 47 | typeOptions: { 48 | minValue: <%= field.numericalLimits.minLimit %>, 49 | maxValue: <%= field.numericalLimits.maxLimit %> 50 | }, 51 | <%_ } _%> 52 | required: <%= field.name !== 'additionalFields' ? true : false %>, 53 | default: <%_ if (field.default === true || field.default === false || typeof field.default === "number") { _%> 54 | <%_ %> <%= field.default _%>, 55 | <%_ } else if (typeof field.default === 'object' && field.default !== null) { _%> 56 | <%_ %> <%= JSON.stringify(field.default) _%>, 57 | <%_ } else { _%> 58 | <%_ %> '<%= field.default _%>', 59 | <%_ } _%> 60 | displayOptions: { 61 | show: { 62 | resource: [ 63 | '<%= h.camelify(resourceName) %>', 64 | ], 65 | operation: [ 66 | '<%= h.camelify(operation.name) %>', 67 | ], 68 | <%_ if (field.extraDisplayRestriction) { _%> 69 | <%= h.camelify(Object.keys(field.extraDisplayRestriction).toString()) %>: [ 70 | <%= Object.values(field.extraDisplayRestriction) %>, 71 | ] 72 | <%_ } _%> 73 | }, 74 | }, 75 | <%_ if (h.isCollective(field)) { _%> 76 | options: [ 77 | <%_ for (let option of field.options) { _%> 78 | { 79 | name: '<%= option.name %>', 80 | description: '<%= option.description %>', 81 | type: '<%= option.type %>', 82 | default: <%_ if (typeof option.default === "boolean" || typeof option.default === "number") { _%> 83 | <%_ %> <%= option.default %>, 84 | <%_ } else if (typeof option.default === 'object' && option.default !== null) { _%> 85 | <%_ %> <%= JSON.stringify(option.default) %>, 86 | <%_ } else { _%> 87 | <%_ %> '<%= option.default _%>', 88 | <%_ } _%> 89 | <%_ if (h.isCollective(option)) { _%> 90 | options: [ 91 | <%_ for (let suboption of option.options) { _%> 92 | { 93 | name: '<%= suboption.name %>', 94 | description: '<%= suboption.description %>', 95 | type: '<%= suboption.type %>', 96 | default: <%_ if (typeof suboption.default === "boolean" || typeof suboption.default === "number") { _%> 97 | <%_ %> <%= suboption.default %>, 98 | <%_ } else if (typeof suboption.default === 'object' && suboption.default !== null) { _%> 99 | <%_ %> <%= JSON.stringify(suboption.default) %>, 100 | <%_ } else { _%> 101 | <%_ %> '<%= suboption.default %>', 102 | <%_ } _%> 103 | <%_ if (h.isCollective(suboption)) { _%> 104 | options: [ 105 | <%_ for (let maxNestedOption of suboption.options) { _%> 106 | { 107 | name: '<%= maxNestedOption.name %>', 108 | description: '<%= maxNestedOption.description %>', 109 | }, 110 | <%_ } _%> 111 | ], 112 | <%_ } _%> 113 | }, 114 | <%_ } _%> 115 | ], 116 | <%_ } _%> 117 | }, 118 | <%_ } _%> 119 | ], 120 | <%_ } _%> 121 | }, 122 | <%_ } _%> 123 | <%_ } _%> 124 | ] as INodeProperties[]; -------------------------------------------------------------------------------- /_templates/gen/generateTriggerNodeSimple/TriggerNodeSimple.ejs.gen: -------------------------------------------------------------------------------- 1 | --- 2 | to: output/<%= name.replace(/\s/g, "") %>Trigger.node.ts 3 | mainParameters: <%= mainParameters %> 4 | metaParameters: <%= metaParameters %> 5 | --- 6 | <%_ 7 | mainParameters = JSON.parse(mainParameters); 8 | metaParameters = JSON.parse(metaParameters); 9 | _%> 10 | import { 11 | IHookFunctions, 12 | IWebhookFunctions, 13 | } from 'n8n-core'; 14 | 15 | import { 16 | IWebhookResponseData, 17 | INodeType, 18 | INodeTypeDescription, 19 | IDataObject, 20 | } from 'n8n-workflow'; 21 | 22 | import { 23 | <%= h.camelify(metaParameters.serviceName) %>ApiRequest, 24 | } from './GenericFunctions'; 25 | 26 | export class <%= h.classify(metaParameters.serviceName) %> implements INodeType { 27 | description: INodeTypeDescription = { 28 | displayName: '<%= metaParameters.serviceName %> Trigger', 29 | name: '<%= h.camelify(metaParameters.serviceName) %>Trigger', 30 | icon: 'file:<%= h.camelify(metaParameters.serviceName) %>.png', 31 | group: ['trigger'], 32 | version: 1, 33 | subtitle: '={{$parameter["event"]}}', 34 | description: 'Handle <%= metaParameters.serviceName %> events via webhooks', 35 | defaults: { 36 | name: '<%= metaParameters.serviceName %>', 37 | color: '<%= metaParameters.nodeColor %>', 38 | }, 39 | inputs: [], 40 | outputs: ['main'], 41 | credentials: [ 42 | { 43 | name: '<%= h.getCredentialsString(h.camelify(metaParameters.serviceName), metaParameters.authType) %>', 44 | required: true, 45 | } 46 | ], 47 | webhooks: [ 48 | { 49 | name: 'default', 50 | httpMethod: 'POST', 51 | responseMode: 'onReceived', 52 | path: 'webhook', 53 | }, 54 | ], 55 | properties: [ 56 | <%_ for (let property of mainParameters.webhookProperties) { _%> 57 | { 58 | displayName: '<%= property.displayName %>', 59 | name: '<%= property.name %>', 60 | type: '<%= property.type %>', 61 | required: <%= property.required %>, 62 | default: <%_ if (property.default === true || property.default === false || typeof property.default === "number") { _%> 63 | <%_ %> <%= property.default _%>, 64 | <%_ } else if (typeof property.default === 'object' && property.default !== null) { _%> 65 | <%_ %> <%= JSON.stringify(property.default) _%>, 66 | <%_ } else { _%> 67 | <%_ %> '<%= property.default _%>', 68 | <%_ } _%> 69 | description: '<%= property.description %>', 70 | options: [ 71 | <%_ for (let option of property.options) { _%> 72 | { 73 | name: '<%= option.name %>', 74 | value: '<%= option.value %>', 75 | description: '<%= option.description %>', 76 | }, 77 | <%_ } _%> 78 | ], 79 | }, 80 | <%_ } _%> 81 | <%_ for (let property of mainParameters.webhookProperties) { _%> 82 | <%_ if (property.options !== undefined) { _%> 83 | <%_ for (let option of property.options) { _%> 84 | <%_ if (option.fields !== undefined) { _%> 85 | <%_ for (let field of option.fields) { _%> 86 | { 87 | displayName: '<%= field.name %>', 88 | name: '<%= field.name %>', 89 | description: '<%= field.description %>', 90 | type: '<%= field.type %>', 91 | required: <%= property.required %>, 92 | default: <%_ if (field.default === true || field.default === false || typeof field.default === "number") { _%> 93 | <%_ %> <%= field.default _%>, 94 | <%_ } else if (typeof field.default === 'object' && field.default !== null) { _%> 95 | <%_ %> <%= JSON.stringify(field.default) _%>, 96 | <%_ } else { _%> 97 | <%_ %> '<%= field.default _%>', 98 | <%_ } _%> 99 | displayOptions: { 100 | show: { 101 | event: [ 102 | '<%= option.value %>', 103 | ], 104 | }, 105 | }, 106 | }, 107 | <%_ } _%> 108 | <%_ } _%> 109 | <%_ } _%> 110 | <%_ } _%> 111 | <%_ } _%> 112 | ], 113 | }; 114 | 115 | // @ts-ignore (because of request) 116 | webhookMethods = { 117 | default: { 118 | async checkExists(this: IHookFunctions): Promise { 119 | const webhookData = this.getWorkflowStaticData('node'); 120 | 121 | if (webhookData.webhookId === undefined) { 122 | // No webhook id is set, so no webhook can exist 123 | return false; 124 | } 125 | 126 | // webhook was created before, so check if it still exists 127 | const endpoint = `webhooks/${webhookData.webhookId}`; // TODO 128 | 129 | try { 130 | await <%= h.camelify(metaParameters.serviceName) %>ApiRequest.call(this, 'GET', endpoint, {}); 131 | } catch (error) { 132 | if (error.statusCode === 404) { 133 | // webhook does not exist 134 | delete webhookData.webhookId; 135 | return false; 136 | } 137 | 138 | // some error occured 139 | throw error; 140 | } 141 | 142 | // if no error, then the webhook exists 143 | return true; 144 | }, 145 | 146 | async create(this: IHookFunctions): Promise { 147 | let webhook; 148 | const webhookUrl = this.getNodeWebhookUrl('default'); 149 | <%_ for (let property of mainParameters.webhookProperties) { _%> 150 | const <%= property.name %> = this.getNodeParameter('<%= property.name %>', 0); 151 | <%_ } _%> 152 | 153 | const endpoint = '<%= mainParameters.webhookEndpoint %>'; 154 | 155 | const qs: IDataObject = {}; 156 | 157 | try { 158 | // TODO: Use node parameters in `qs` or in `body` or as boolean flag 159 | webhook = await <%= h.camelify(metaParameters.serviceName) %>ApiRequest.call(this, 'POST', endpoint, {}, qs); 160 | } catch (error) { 161 | throw error; 162 | } 163 | 164 | // TODO: Replace TODO_PROPERTY_NAME with the name of the property 165 | // that, if missing, indicates that webhook creation has failed 166 | if (TODO_PROPERTY_NAME === undefined) { 167 | return false; 168 | } 169 | 170 | const webhookData = this.getWorkflowStaticData('node'); 171 | webhookData.webhookId = webhook.rule.id as string; // TODO 172 | webhookData.events = event; // TODO 173 | return true; 174 | }, 175 | 176 | async delete(this: IHookFunctions): Promise { 177 | const webhookData = this.getWorkflowStaticData('node'); 178 | 179 | if (webhookData.webhookId !== undefined) { 180 | const endpoint = `/automations/hooks/${webhookData.webhookId}`; 181 | 182 | try { 183 | await <%= h.camelify(metaParameters.serviceName) %>ApiRequest.call(this, 'DELETE', endpoint, {}, {}); 184 | } catch (error) { 185 | return false; 186 | } 187 | 188 | // Remove properties from static workflow data to 189 | // make it clear that no webhooks are registred anymore 190 | delete webhookData.webhookId; 191 | delete webhookData.events; 192 | } 193 | return true; 194 | }, 195 | }, 196 | }; 197 | 198 | async webhook(this: IWebhookFunctions): Promise { 199 | const returnData: IDataObject[] = []; 200 | returnData.push(this.getBodyData()); 201 | 202 | return { 203 | workflowData: [ 204 | this.helpers.returnJsonArray(returnData), 205 | ], 206 | }; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /_templates/gen/updateCredentialPackageJson/packageJsonCredential.ejs.inj: -------------------------------------------------------------------------------- 1 | --- 2 | inject: true 3 | credentialSpot: <%= credentialSpot %> 4 | serviceCredential: <%= serviceCredential %> 5 | to: output/package.json 6 | before: \s+"dist\/credentials\/<%= credentialSpot %>.credentials.js", 7 | --- 8 | "dist/credentials/<%= serviceCredential %>.credentials.js", -------------------------------------------------------------------------------- /_templates/gen/updateNodePackageJson/packageJsonNode.ejs.inj: -------------------------------------------------------------------------------- 1 | --- 2 | inject: true 3 | nodeSpot: <%= nodeSpot %> 4 | serviceName: <%= serviceName %> 5 | to: output/package.json 6 | before: \s+"dist\/nodes\/<%= nodeSpot %>.node.js", 7 | --- 8 | "dist/nodes/<%= serviceName %>/<%= serviceName %>.node.js", -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLH-Fellowship/nodemaker/659c888332823f462eaff0148bab204e2be4c33f/build/.gitkeep -------------------------------------------------------------------------------- /client/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended', 9 | '@vue/typescript' 10 | ], 11 | parserOptions: { 12 | parser: '@typescript-eslint/parser' 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | #Electron-builder output 25 | /dist_electron -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /client/Requester.ts: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = window.require("electron"); 2 | 3 | /**Responsible for receiving a request through a channel and returning a response through that same channel.*/ 4 | export default class Requester { 5 | // prettier-ignore 6 | public request(channel: string, argument?: T): Promise> { 7 | ipcRenderer.send(channel, argument); 8 | 9 | return new Promise((resolve) => { 10 | ipcRenderer.on(channel, (event, response) => resolve(response)); 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/assests/n8n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLH-Fellowship/nodemaker/659c888332823f462eaff0148bab204e2be4c33f/client/assests/n8n.png -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/channels/DocsgenChannel.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from "electron"; 2 | import IpcChannel from "./IpcChannel.interface"; 3 | import NodeDocsGenerator from "../../generators/NodeDocsGenerator"; 4 | 5 | export default class DocsgenChannel implements IpcChannel { 6 | public name = "docsgen-channel"; 7 | 8 | public async handle(event: IpcMainEvent, paramsBundle: DocsgenParamsBundle) { 9 | process.chdir(".."); 10 | const generator = new NodeDocsGenerator(paramsBundle); 11 | const result = await generator.run(); 12 | process.chdir("client"); 13 | 14 | event.sender.send(this.name, result); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/channels/EmptyChannel.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from "electron"; 2 | import IpcChannel from "./IpcChannel.interface"; 3 | import DirectoryEmptier from "../../services/DirectoryEmptier"; 4 | 5 | export default class EmptyChannel implements IpcChannel { 6 | public name = "empty-channel"; 7 | 8 | public async handle(event: IpcMainEvent) { 9 | process.chdir(".."); 10 | const emptier = new DirectoryEmptier(); 11 | const result = await emptier.run(); 12 | process.chdir("client"); 13 | 14 | event.sender.send(this.name, result); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/channels/ExampleChannel.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from "electron"; 2 | import IpcChannel from "./IpcChannel.interface"; 3 | 4 | export default class ExampleChannel implements IpcChannel { 5 | public name = "example-channel"; 6 | 7 | public async handle(event: IpcMainEvent) { 8 | event.sender.send(this.name, "hello my name is erin"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/channels/IpcChannel.interface.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from "electron"; 2 | 3 | export default interface IpcChannel { 4 | name: string; 5 | handle(event: IpcMainEvent, argument?: any): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /client/channels/NodegenChannel.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from "electron"; 2 | import IpcChannel from "./IpcChannel.interface"; 3 | import NodeFilesGenerator from "../../generators/NodeFilesGenerator"; 4 | 5 | export default class NodegenChannel implements IpcChannel { 6 | public name = "nodegen-channel"; 7 | 8 | public async handle(event: IpcMainEvent, paramsBundle: NodegenParamsBundle) { 9 | process.chdir(".."); 10 | const generator = new NodeFilesGenerator(paramsBundle); 11 | const result = await generator.run(); 12 | process.chdir("client"); 13 | 14 | event.sender.send(this.name, result); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/channels/PackgenChannel.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from "electron"; 2 | import IpcChannel from "./IpcChannel.interface"; 3 | import PackageJsonGenerator from "../../generators/PackageJsonGenerator"; 4 | 5 | export default class PackgenChannel implements IpcChannel { 6 | public name = "packgen-channel"; 7 | 8 | public async handle(event: IpcMainEvent, metaParameters: MetaParameters) { 9 | process.chdir(".."); 10 | const generator = new PackageJsonGenerator(metaParameters); 11 | const result = await generator.run(); 12 | process.chdir("client"); 13 | 14 | event.sender.send(this.name, result); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/channels/PlacementChannel.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainEvent } from "electron"; 2 | import IpcChannel from "./IpcChannel.interface"; 3 | import FilePlacer from "../../services/FilePlacer"; 4 | 5 | export default class PlacementChannel implements IpcChannel { 6 | public name = "placement-channel"; 7 | 8 | public async handle( 9 | event: IpcMainEvent, 10 | { filesToPlace }: PlacementChannelArgument 11 | ) { 12 | process.chdir(".."); 13 | 14 | const filePlacer = new FilePlacer(); 15 | 16 | const result = 17 | filesToPlace === "functionality" 18 | ? await filePlacer.placeNodeFunctionalityFiles() 19 | : await filePlacer.placeNodeDocumentationFiles(); 20 | 21 | process.chdir("client"); 22 | 23 | event.sender.send(this.name, result); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "electron:build": "vue-cli-service electron:build", 10 | "electron:serve": "vue-cli-service electron:serve", 11 | "postinstall": "electron-builder install-app-deps", 12 | "postuninstall": "electron-builder install-app-deps" 13 | }, 14 | "main": "background.js", 15 | "dependencies": { 16 | "core-js": "^3.6.5", 17 | "vue": "^2.6.11", 18 | "vue-class-component": "^7.2.3", 19 | "vue-property-decorator": "^8.4.2", 20 | "vue-router": "^3.2.0", 21 | "vuex": "^3.4.0" 22 | }, 23 | "devDependencies": { 24 | "@types/electron-devtools-installer": "^2.2.0", 25 | "@types/node": "^12.12.38", 26 | "@typescript-eslint/eslint-plugin": "^2.33.0", 27 | "@typescript-eslint/parser": "^2.33.0", 28 | "@vue/cli-plugin-babel": "~4.4.0", 29 | "@vue/cli-plugin-eslint": "~4.4.0", 30 | "@vue/cli-plugin-router": "~4.4.0", 31 | "@vue/cli-plugin-typescript": "^4.4.6", 32 | "@vue/cli-plugin-vuex": "^4.4.6", 33 | "@vue/cli-service": "~4.4.0", 34 | "@vue/eslint-config-typescript": "^5.0.2", 35 | "babel-eslint": "^10.1.0", 36 | "electron": "^9.0.0", 37 | "electron-devtools-installer": "^3.1.0", 38 | "eslint": "^6.7.2", 39 | "eslint-plugin-vue": "^6.2.2", 40 | "typescript": "~3.9.3", 41 | "vue-cli-plugin-electron-builder": "^2.0.0-rc.4", 42 | "vue-template-compiler": "^2.6.11" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLH-Fellowship/nodemaker/659c888332823f462eaff0148bab204e2be4c33f/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= htmlWebpackPlugin.options.title %> 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 102 | -------------------------------------------------------------------------------- /client/src/assets/n8n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLH-Fellowship/nodemaker/659c888332823f462eaff0148bab204e2be4c33f/client/src/assets/n8n.png -------------------------------------------------------------------------------- /client/src/assets/n8n_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLH-Fellowship/nodemaker/659c888332823f462eaff0148bab204e2be4c33f/client/src/assets/n8n_copy.png -------------------------------------------------------------------------------- /client/src/background.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from "fs"; 2 | import { join } from "path"; 3 | import { app, protocol, BrowserWindow, ipcMain } from "electron"; 4 | import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; 5 | import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; 6 | import IpcChannel from "../channels/IpcChannel.interface"; 7 | 8 | // Vue boilerplate 9 | protocol.registerSchemesAsPrivileged([ 10 | { scheme: "app", privileges: { secure: true, standard: true } }, 11 | ]); 12 | 13 | /**Responsible for managing the Electron app (main process), the various windows (renderer processes), and IPC channels.*/ 14 | class Client { 15 | mainWindow: BrowserWindow | null; // first renderer process 16 | 17 | constructor() { 18 | app.on("ready", async () => { 19 | this.installVueDevTools(); 20 | this.createMainWindow(); 21 | await this.registerIpcChannels(); 22 | }); 23 | app.on("window-all-closed", app.quit); 24 | app.allowRendererProcessReuse = true; 25 | } 26 | 27 | // Vue boilerplate 28 | private installVueDevTools() { 29 | if (process.env.NODE_ENV !== "production" && !process.env.IS_TEST) { 30 | try { 31 | installExtension(VUEJS_DEVTOOLS); 32 | } catch (error) { 33 | console.error("Vue Devtools failed to install: ", error.toString()); 34 | } 35 | } 36 | } 37 | 38 | private createMainWindow() { 39 | this.mainWindow = new BrowserWindow({ 40 | width: 1000, 41 | height: 600, 42 | resizable: false, 43 | webPreferences: { nodeIntegration: true }, 44 | }); 45 | 46 | // Vue boilerplate 47 | if (process.env.WEBPACK_DEV_SERVER_URL) { 48 | this.mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL); 49 | if (!process.env.IS_TEST) { 50 | this.mainWindow.webContents.openDevTools(); 51 | } 52 | } else { 53 | createProtocol("app"); 54 | this.mainWindow.loadURL("app://./index.html"); 55 | } 56 | 57 | this.mainWindow.on("closed", () => { 58 | this.mainWindow = null; 59 | }); 60 | } 61 | 62 | /**Registers all the IPC channels for handling requests from the renderer process.*/ 63 | private async registerIpcChannels() { 64 | const ipcChannels: IpcChannel[] = []; 65 | 66 | for (let module of this.getChannelModules()) { 67 | const ChannelClass = require(`../channels/${module}`).default; // dynamic import 68 | ipcChannels.push(new ChannelClass()); 69 | } 70 | 71 | ipcChannels.forEach((channel) => 72 | ipcMain.on(channel.name, (event, argument?: any) => 73 | channel.handle(event, argument) 74 | ) 75 | ); 76 | } 77 | 78 | private getChannelModules() { 79 | return readdirSync(join("channels")).filter( 80 | (channel) => !channel.endsWith(".interface.ts") 81 | ); 82 | } 83 | } 84 | 85 | new Client(); 86 | -------------------------------------------------------------------------------- /client/src/components/AddButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /client/src/components/BackwardButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 54 | -------------------------------------------------------------------------------- /client/src/components/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 77 | -------------------------------------------------------------------------------- /client/src/components/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | 41 | 75 | -------------------------------------------------------------------------------- /client/src/components/ForwardButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /client/src/components/GenericButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/components/InputField.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | 37 | 70 | -------------------------------------------------------------------------------- /client/src/components/Instructions.vue: -------------------------------------------------------------------------------- 1 | : 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /client/src/components/SmallButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /client/src/components/Switch.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | -------------------------------------------------------------------------------- /client/src/components/TextArea.vue: -------------------------------------------------------------------------------- 1 |