Add nodes generated by the Node CLI, update README (#96)

This commit is contained in:
Elias Meire 2025-10-16 09:44:41 +02:00 committed by GitHub
commit 4fb0cd0bc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 8549 additions and 745 deletions

View file

@ -1,20 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[package.json]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_style = space
indent_size = 2

View file

@ -1,146 +0,0 @@
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
root: true,
env: {
browser: true,
es6: true,
node: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json'],
sourceType: 'module',
extraFileExtensions: ['.json'],
},
ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'],
overrides: [
{
files: ['package.json'],
plugins: ['eslint-plugin-n8n-nodes-base'],
extends: ['plugin:n8n-nodes-base/community'],
rules: {
'n8n-nodes-base/community-package-json-name-still-default': 'off',
},
},
{
files: ['./credentials/**/*.ts'],
plugins: ['eslint-plugin-n8n-nodes-base'],
extends: ['plugin:n8n-nodes-base/credentials'],
rules: {
'n8n-nodes-base/cred-class-field-authenticate-type-assertion': 'error',
'n8n-nodes-base/cred-class-field-display-name-missing-oauth2': 'error',
'n8n-nodes-base/cred-class-field-display-name-miscased': 'error',
'n8n-nodes-base/cred-class-field-documentation-url-missing': 'error',
'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off',
'n8n-nodes-base/cred-class-field-name-missing-oauth2': 'error',
'n8n-nodes-base/cred-class-field-name-unsuffixed': 'error',
'n8n-nodes-base/cred-class-field-name-uppercase-first-char': 'error',
'n8n-nodes-base/cred-class-field-properties-assertion': 'error',
'n8n-nodes-base/cred-class-field-type-options-password-missing': 'error',
'n8n-nodes-base/cred-class-name-missing-oauth2-suffix': 'error',
'n8n-nodes-base/cred-class-name-unsuffixed': 'error',
'n8n-nodes-base/cred-filename-against-convention': 'error',
},
},
{
files: ['./nodes/**/*.ts'],
plugins: ['eslint-plugin-n8n-nodes-base'],
extends: ['plugin:n8n-nodes-base/nodes'],
rules: {
'n8n-nodes-base/node-class-description-credentials-name-unsuffixed': 'error',
'n8n-nodes-base/node-class-description-display-name-unsuffixed-trigger-node': 'error',
'n8n-nodes-base/node-class-description-empty-string': 'error',
'n8n-nodes-base/node-class-description-icon-not-svg': 'error',
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off',
'n8n-nodes-base/node-class-description-inputs-wrong-trigger-node': 'error',
'n8n-nodes-base/node-class-description-missing-subtitle': 'error',
'n8n-nodes-base/node-class-description-non-core-color-present': 'error',
'n8n-nodes-base/node-class-description-name-miscased': 'error',
'n8n-nodes-base/node-class-description-name-unsuffixed-trigger-node': 'error',
'n8n-nodes-base/node-class-description-outputs-wrong': 'off',
'n8n-nodes-base/node-dirname-against-convention': 'error',
'n8n-nodes-base/node-execute-block-double-assertion-for-items': 'error',
'n8n-nodes-base/node-execute-block-wrong-error-thrown': 'error',
'n8n-nodes-base/node-filename-against-convention': 'error',
'n8n-nodes-base/node-param-array-type-assertion': 'error',
'n8n-nodes-base/node-param-color-type-unused': 'error',
'n8n-nodes-base/node-param-default-missing': 'error',
'n8n-nodes-base/node-param-default-wrong-for-boolean': 'error',
'n8n-nodes-base/node-param-default-wrong-for-collection': 'error',
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
'n8n-nodes-base/node-param-default-wrong-for-multi-options': 'error',
'n8n-nodes-base/node-param-default-wrong-for-number': 'error',
'n8n-nodes-base/node-param-default-wrong-for-simplify': 'error',
'n8n-nodes-base/node-param-default-wrong-for-string': 'error',
'n8n-nodes-base/node-param-description-boolean-without-whether': 'error',
'n8n-nodes-base/node-param-description-comma-separated-hyphen': 'error',
'n8n-nodes-base/node-param-description-empty-string': 'error',
'n8n-nodes-base/node-param-description-excess-final-period': 'error',
'n8n-nodes-base/node-param-description-excess-inner-whitespace': 'error',
'n8n-nodes-base/node-param-description-identical-to-display-name': 'error',
'n8n-nodes-base/node-param-description-line-break-html-tag': 'error',
'n8n-nodes-base/node-param-description-lowercase-first-char': 'error',
'n8n-nodes-base/node-param-description-miscased-id': 'error',
'n8n-nodes-base/node-param-description-miscased-json': 'error',
'n8n-nodes-base/node-param-description-miscased-url': 'error',
'n8n-nodes-base/node-param-description-missing-final-period': 'error',
'n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues': 'error',
'n8n-nodes-base/node-param-description-missing-for-return-all': 'error',
'n8n-nodes-base/node-param-description-missing-for-simplify': 'error',
'n8n-nodes-base/node-param-description-missing-from-dynamic-multi-options': 'error',
'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'error',
'n8n-nodes-base/node-param-description-missing-from-limit': 'error',
'n8n-nodes-base/node-param-description-unencoded-angle-brackets': 'error',
'n8n-nodes-base/node-param-description-unneeded-backticks': 'error',
'n8n-nodes-base/node-param-description-untrimmed': 'error',
'n8n-nodes-base/node-param-description-url-missing-protocol': 'error',
'n8n-nodes-base/node-param-description-weak': 'error',
'n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options': 'error',
'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'error',
'n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues': 'error',
'n8n-nodes-base/node-param-description-wrong-for-limit': 'error',
'n8n-nodes-base/node-param-description-wrong-for-return-all': 'error',
'n8n-nodes-base/node-param-description-wrong-for-simplify': 'error',
'n8n-nodes-base/node-param-description-wrong-for-upsert': 'error',
'n8n-nodes-base/node-param-display-name-excess-inner-whitespace': 'error',
'n8n-nodes-base/node-param-display-name-miscased-id': 'error',
'n8n-nodes-base/node-param-display-name-miscased': 'error',
'n8n-nodes-base/node-param-display-name-not-first-position': 'error',
'n8n-nodes-base/node-param-display-name-untrimmed': 'error',
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options': 'error',
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'error',
'n8n-nodes-base/node-param-display-name-wrong-for-simplify': 'error',
'n8n-nodes-base/node-param-display-name-wrong-for-update-fields': 'error',
'n8n-nodes-base/node-param-min-value-wrong-for-limit': 'error',
'n8n-nodes-base/node-param-multi-options-type-unsorted-items': 'error',
'n8n-nodes-base/node-param-name-untrimmed': 'error',
'n8n-nodes-base/node-param-operation-option-action-wrong-for-get-many': 'error',
'n8n-nodes-base/node-param-operation-option-description-wrong-for-get-many': 'error',
'n8n-nodes-base/node-param-operation-option-without-action': 'error',
'n8n-nodes-base/node-param-operation-without-no-data-expression': 'error',
'n8n-nodes-base/node-param-option-description-identical-to-name': 'error',
'n8n-nodes-base/node-param-option-name-containing-star': 'error',
'n8n-nodes-base/node-param-option-name-duplicate': 'error',
'n8n-nodes-base/node-param-option-name-wrong-for-get-many': 'error',
'n8n-nodes-base/node-param-option-name-wrong-for-upsert': 'error',
'n8n-nodes-base/node-param-option-value-duplicate': 'error',
'n8n-nodes-base/node-param-options-type-unsorted-items': 'error',
'n8n-nodes-base/node-param-placeholder-miscased-id': 'error',
'n8n-nodes-base/node-param-placeholder-missing-email': 'error',
'n8n-nodes-base/node-param-required-false': 'error',
'n8n-nodes-base/node-param-resource-with-plural-option': 'error',
'n8n-nodes-base/node-param-resource-without-no-data-expression': 'error',
'n8n-nodes-base/node-param-type-options-missing-from-limit': 'error',
'n8n-nodes-base/node-param-type-options-password-missing': 'error',
},
},
],
};

View file

@ -1,16 +0,0 @@
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: "./.eslintrc.js",
overrides: [
{
files: ['package.json'],
plugins: ['eslint-plugin-n8n-nodes-base'],
rules: {
'n8n-nodes-base/community-package-json-name-still-default': 'error',
},
},
],
};

28
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: 'npm ci'
- name: Run lint
run: 'npm run lint'
- name: Run build
run: 'npm run build'

8
.gitignore vendored
View file

@ -1,8 +1,2 @@
node_modules
.DS_Store
.tmp
tmp
dist dist
npm-debug.log* node_modules
yarn.lock
.vscode/launch.json

View file

@ -1,2 +0,0 @@
.DS_Store
*.tsbuildinfo

12
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to running n8n",
"processId": "${command:PickProcess}",
"request": "attach",
"skipFiles": ["<node_internals>/**"],
"type": "node"
}
]
}

257
README.md
View file

@ -2,46 +2,245 @@
# n8n-nodes-starter # n8n-nodes-starter
This repo contains example nodes to help you get started building your own custom integrations for [n8n](https://n8n.io). It includes the node linter and other dependencies. This starter repository helps you build custom integrations for [n8n](https://n8n.io). It includes example nodes, credentials, the node linter, and all the tooling you need to get started.
To make your custom node available to the community, you must create it as an npm package, and [submit it to the npm registry](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry). ## Quick Start
If you would like your node to be available on n8n cloud you can also [submit your node for verification](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/). > [!TIP]
> **New to building n8n nodes?** The fastest way to get started is with `npm create @n8n/node`. This command scaffolds a complete node package for you using the [@n8n/node-cli](https://www.npmjs.com/package/@n8n/node-cli).
**To create a new node package from scratch:**
```bash
npm create @n8n/node
```
**Already using this starter? Start developing with:**
```bash
npm run dev
```
This starts n8n with your nodes loaded and hot reload enabled.
## What's Included
This starter repository includes two example nodes to learn from:
- **[Example Node](nodes/Example/)** - A simple starter node that shows the basic structure with a custom `execute` method
- **[GitHub Issues Node](nodes/GithubIssues/)** - A complete, production-ready example built using the **declarative style**:
- **Low-code approach** - Define operations declaratively without writing request logic
- Multiple resources (Issues, Comments)
- Multiple operations (Get, Get All, Create)
- Two authentication methods (OAuth2 and Personal Access Token)
- List search functionality for dynamic dropdowns
- Proper error handling and typing
- Ideal for HTTP API-based integrations
> [!TIP]
> The declarative/low-code style (used in GitHub Issues) is the recommended approach for building nodes that interact with HTTP APIs. It significantly reduces boilerplate code and handles requests automatically.
Browse these examples to understand both approaches, then modify them or create your own.
## Finding Inspiration
Looking for more examples? Check out these resources:
- **[npm Community Nodes](https://www.npmjs.com/search?q=keywords:n8n-community-node-package)** - Browse thousands of community-built nodes on npm using the `n8n-community-node-package` tag
- **[n8n Built-in Nodes](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes)** - Study the source code of n8n's official nodes for production-ready patterns and best practices
- **[n8n Credentials](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/credentials)** - See how authentication is implemented for various services
These are excellent resources to understand how to structure your nodes, handle different API patterns, and implement advanced features.
## Prerequisites ## Prerequisites
You need the following installed on your development machine: Before you begin, install the following on your development machine:
* [git](https://git-scm.com/downloads) ### Required
* Node.js and npm. Minimum version Node 20. You can find instructions on how to install both using nvm (Node Version Manager) for Linux, Mac, and WSL [here](https://github.com/nvm-sh/nvm). For Windows users, refer to Microsoft's guide to [Install NodeJS on Windows](https://docs.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows).
* Install n8n with:
```
npm install n8n -g
```
* Recommended: follow n8n's guide to [set up your development environment](https://docs.n8n.io/integrations/creating-nodes/build/node-development-environment/).
## Using this starter - **[Node.js](https://nodejs.org/)** (v20 or higher) and npm
- Linux/Mac/WSL: Install via [nvm](https://github.com/nvm-sh/nvm)
- Windows: Follow [Microsoft's NodeJS guide](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows)
- **[git](https://git-scm.com/downloads)**
These are the basic steps for working with the starter. For detailed guidance on creating and publishing nodes, refer to the [documentation](https://docs.n8n.io/integrations/creating-nodes/). ### Recommended
1. [Generate a new repository](https://github.com/n8n-io/n8n-nodes-starter/generate) from this template repository. - Follow n8n's [development environment setup guide](https://docs.n8n.io/integrations/creating-nodes/build/node-development-environment/)
2. Clone your new repo:
```
git clone https://github.com/<your organization>/<your-repo-name>.git
```
3. Run `npm i` to install dependencies.
4. Open the project in your editor.
5. Browse the examples in `/nodes` and `/credentials`. Modify the examples, or replace them with your own nodes.
6. Update the `package.json` to match your details.
7. Run `npm run lint` to check for errors or `npm run lintfix` to automatically fix errors when possible.
8. Test your node locally. Refer to [Run your node locally](https://docs.n8n.io/integrations/creating-nodes/test/run-node-locally/) for guidance.
9. Replace this README with documentation for your node. Use the [README_TEMPLATE](README_TEMPLATE.md) to get started.
10. Update the LICENSE file to use your details.
11. [Publish](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry) your package to npm.
## More information > [!NOTE]
> The `@n8n/node-cli` is included as a dev dependency and will be installed automatically when you run `npm install`. The CLI includes n8n for local development, so you don't need to install n8n globally.
Refer to our [documentation on creating nodes](https://docs.n8n.io/integrations/creating-nodes/) for detailed information on building your own nodes. ## Getting Started with this Starter
Follow these steps to create your own n8n community node package:
### 1. Create Your Repository
[Generate a new repository](https://github.com/n8n-io/n8n-nodes-starter/generate) from this template, then clone it:
```bash
git clone https://github.com/<your-organization>/<your-repo-name>.git
cd <your-repo-name>
```
### 2. Install Dependencies
```bash
npm install
```
This installs all required dependencies including the `@n8n/node-cli`.
### 3. Explore the Examples
Browse the example nodes in [nodes/](nodes/) and [credentials/](credentials/) to understand the structure:
- Start with [nodes/Example/](nodes/Example/) for a basic node
- Study [nodes/GithubIssues/](nodes/GithubIssues/) for a real-world implementation
### 4. Build Your Node
Edit the example nodes to fit your use case, or create new node files by copying the structure from [nodes/Example/](nodes/Example/).
> [!TIP]
> If you want to scaffold a completely new node package, use `npm create @n8n/node` to start fresh with the CLI's interactive generator.
### 5. Configure Your Package
Update `package.json` with your details:
- `name` - Your package name (must start with `n8n-nodes-`)
- `author` - Your name and email
- `repository` - Your repository URL
- `description` - What your node does
Make sure your node is registered in the `n8n.nodes` array.
### 6. Develop and Test Locally
Start n8n with your node loaded:
```bash
npm run dev
```
This command runs `n8n-node dev` which:
- Builds your node with watch mode
- Starts n8n with your node available
- Automatically rebuilds when you make changes
- Opens n8n in your browser (usually http://localhost:5678)
You can now test your node in n8n workflows!
> [!NOTE]
> Learn more about CLI commands in the [@n8n/node-cli documentation](https://www.npmjs.com/package/@n8n/node-cli).
### 7. Lint Your Code
Check for errors:
```bash
npm run lint
```
Auto-fix issues when possible:
```bash
npm run lint:fix
```
### 8. Build for Production
When ready to publish:
```bash
npm run build
```
This compiles your TypeScript code to the `dist/` folder.
### 9. Prepare for Publishing
Before publishing:
1. **Update documentation**: Replace this README with your node's documentation. Use [README_TEMPLATE.md](README_TEMPLATE.md) as a starting point.
2. **Update the LICENSE**: Add your details to the [LICENSE](LICENSE.md) file.
3. **Test thoroughly**: Ensure your node works in different scenarios.
### 10. Publish to npm
Publish your package to make it available to the n8n community:
```bash
npm publish
```
Learn more about [publishing to npm](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry).
### 11. Submit for Verification (Optional)
Get your node verified for n8n Cloud:
1. Ensure your node meets the [requirements](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/):
- Uses MIT license ✅ (included in this starter)
- No external package dependencies
- Follows n8n's design guidelines
- Passes quality and security review
2. Submit through the [n8n Creator Portal](https://creators.n8n.io/nodes)
**Benefits of verification:**
- Available directly in n8n Cloud
- Discoverable in the n8n nodes panel
- Verified badge for quality assurance
- Increased visibility in the n8n community
## Available Scripts
This starter includes several npm scripts to streamline development:
| Script | Description |
| --------------------- | ---------------------------------------------------------------- |
| `npm run dev` | Start n8n with your node and watch for changes (runs `n8n-node dev`) |
| `npm run build` | Compile TypeScript to JavaScript for production (runs `n8n-node build`) |
| `npm run build:watch` | Build in watch mode (auto-rebuild on changes) |
| `npm run lint` | Check your code for errors and style issues (runs `n8n-node lint`) |
| `npm run lint:fix` | Automatically fix linting issues when possible (runs `n8n-node lint --fix`) |
| `npm run release` | Create a new release (runs `n8n-node release`) |
> [!TIP]
> These scripts use the [@n8n/node-cli](https://www.npmjs.com/package/@n8n/node-cli) under the hood. You can also run CLI commands directly, e.g., `npx n8n-node dev`.
## Troubleshooting
### My node doesn't appear in n8n
1. Make sure you ran `npm install` to install dependencies
2. Check that your node is listed in `package.json` under `n8n.nodes`
3. Restart the dev server with `npm run dev`
4. Check the console for any error messages
### Linting errors
Run `npm run lint:fix` to automatically fix most common issues. For remaining errors, check the [n8n node development guidelines](https://docs.n8n.io/integrations/creating-nodes/).
### TypeScript errors
Make sure you're using Node.js v20 or higher and have run `npm install` to get all type definitions.
## Resources
- **[n8n Node Documentation](https://docs.n8n.io/integrations/creating-nodes/)** - Complete guide to building nodes
- **[n8n Community Forum](https://community.n8n.io/)** - Get help and share your nodes
- **[@n8n/node-cli Documentation](https://www.npmjs.com/package/@n8n/node-cli)** - CLI tool reference
- **[n8n Creator Portal](https://creators.n8n.io/nodes)** - Submit your node for verification
- **[Submit Community Nodes Guide](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/)** - Verification requirements and process
## Contributing
Have suggestions for improving this starter? [Open an issue](https://github.com/n8n-io/n8n-nodes-starter/issues) or submit a pull request!
## License ## License

View file

@ -1,59 +0,0 @@
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class ExampleCredentialsApi implements ICredentialType {
name = 'exampleCredentialsApi';
displayName = 'Example Credentials API';
documentationUrl = 'https://your-docs-url';
properties: INodeProperties[] = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'User Name',
name: 'username',
type: 'string',
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
typeOptions: {
password: true,
},
default: '',
},
];
// This credential is currently not used by any node directly
// but the HTTP Request node can use it to make requests.
// The credential is also testable due to the `test` property below
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
auth: {
username: '={{ $credentials.username }}',
password: '={{ $credentials.password }}',
},
qs: {
// Send this as part of the query string
n8n: 'rocks',
},
},
};
// The block below tells how this credential can be tested
test: ICredentialTestRequest = {
request: {
baseURL: 'https://example.com/',
url: '',
},
};
}

View file

@ -0,0 +1,45 @@
import type {
IAuthenticateGeneric,
Icon,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class GithubIssuesApi implements ICredentialType {
name = 'githubIssuesApi';
displayName = 'GitHub Issues API';
icon: Icon = { light: 'file:../icons/github.svg', dark: 'file:../icons/github.dark.svg' };
documentationUrl =
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#deleting-a-personal-access-token';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
typeOptions: { password: true },
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=token {{$credentials?.accessToken}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.github.com',
url: '/user',
method: 'GET',
},
};
}

View file

@ -0,0 +1,54 @@
import type { Icon, ICredentialType, INodeProperties } from 'n8n-workflow';
export class GithubIssuesOAuth2Api implements ICredentialType {
name = 'githubIssuesOAuth2Api';
extends = ['oAuth2Api'];
displayName = 'GitHub Issues OAuth2 API';
icon: Icon = { light: 'file:../icons/github.svg', dark: 'file:../icons/github.dark.svg' };
documentationUrl = 'https://docs.github.com/en/apps/oauth-apps';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'authorizationCode',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: 'https://github.com/login/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://github.com/login/oauth/access_token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'repo',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'header',
},
];
}

View file

@ -1,50 +0,0 @@
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class HttpBinApi implements ICredentialType {
name = 'httpbinApi';
displayName = 'HttpBin API';
documentationUrl = 'https://your-docs-url';
properties: INodeProperties[] = [
{
displayName: 'Token',
name: 'token',
type: 'string',
default: '',
typeOptions: {
password: true,
}
},
{
displayName: 'Domain',
name: 'domain',
type: 'string',
default: 'https://httpbin.org',
},
];
// This allows the credential to be used by other parts of n8n
// stating how this credential is injected as part of the request
// An example is the Http Request node that can make generic calls
// reusing this credential
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '={{"Bearer " + $credentials.token}}',
},
},
};
// The block below tells how this credential can be tested
test: ICredentialTestRequest = {
request: {
baseURL: '={{$credentials?.domain}}',
url: '/bearer',
},
};
}

3
eslint.config.mjs Normal file
View file

@ -0,0 +1,3 @@
import { config } from '@n8n/node-cli/eslint';
export default config;

View file

@ -1,16 +0,0 @@
const path = require('path');
const { task, src, dest } = require('gulp');
task('build:icons', copyIcons);
function copyIcons() {
const nodeSource = path.resolve('nodes', '**', '*.{png,svg}');
const nodeDestination = path.resolve('dist', 'nodes');
src(nodeSource).pipe(dest(nodeDestination));
const credSource = path.resolve('credentials', '**', '*.{png,svg}');
const credDestination = path.resolve('dist', 'credentials');
return src(credSource).pipe(dest(credDestination));
}

3
icons/github.dark.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

3
icons/github.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="#24292F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,17 +1,17 @@
{ {
"node": "n8n-nodes-base.httpbin", "node": "n8n-nodes-example",
"nodeVersion": "1.0", "nodeVersion": "1.0",
"codexVersion": "1.0", "codexVersion": "1.0",
"categories": ["Development", "Developer Tools"], "categories": ["Development", "Developer Tools"],
"resources": { "resources": {
"credentialDocumentation": [ "credentialDocumentation": [
{ {
"url": "http://httpbin.org/#/Auth/get_bearer" "url": "https://github.com/org/repo?tab=readme-ov-file#credentials"
} }
], ],
"primaryDocumentation": [ "primaryDocumentation": [
{ {
"url": "http://httpbin.org/" "url": "https://github.com/org/repo?tab=readme-ov-file"
} }
] ]
} }

View file

@ -6,15 +6,16 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
export class ExampleNode implements INodeType { export class Example implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Example Node', displayName: 'Example',
name: 'exampleNode', name: 'example',
group: ['transform'], icon: { light: 'file:example.svg', dark: 'file:example.dark.svg' },
group: ['input'],
version: 1, version: 1,
description: 'Basic Example Node', description: 'Basic Example Node',
defaults: { defaults: {
name: 'Example Node', name: 'Example',
}, },
inputs: [NodeConnectionType.Main], inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],

View file

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="aquamarine"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 698 B

13
nodes/Example/example.svg Normal file
View file

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="darkblue"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 696 B

View file

@ -0,0 +1,18 @@
{
"node": "n8n-nodes-github-issues",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file#credentials"
}
],
"primaryDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file"
}
]
}
}

View file

@ -0,0 +1,96 @@
import { NodeConnectionType, type INodeType, type INodeTypeDescription } from 'n8n-workflow';
import { issueDescription } from './resources/issue';
import { issueCommentDescription } from './resources/issueComment';
import { getRepositories } from './listSearch/getRepositories';
import { getUsers } from './listSearch/getUsers';
import { getIssues } from './listSearch/getIssues';
export class GithubIssues implements INodeType {
description: INodeTypeDescription = {
displayName: 'GitHub Issues',
name: 'githubIssues',
icon: { light: 'file:../../icons/github.svg', dark: 'file:../../icons/github.dark.svg' },
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume issues from the GitHub API',
defaults: {
name: 'GitHub Issues',
},
usableAsTool: true,
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'githubIssuesApi',
required: true,
displayOptions: {
show: {
authentication: ['accessToken'],
},
},
},
{
name: 'githubIssuesOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
requestDefaults: {
baseURL: 'https://api.github.com',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Issue',
value: 'issue',
},
{
name: 'Issue Comment',
value: 'issueComment',
},
],
default: 'issue',
},
...issueDescription,
...issueCommentDescription,
],
};
methods = {
listSearch: {
getRepositories,
getUsers,
getIssues,
},
};
}

View file

@ -0,0 +1,49 @@
import type {
ILoadOptionsFunctions,
INodeListSearchResult,
INodeListSearchItems,
} from 'n8n-workflow';
import { githubApiRequest } from '../shared/transport';
type IssueSearchItem = {
number: number;
title: string;
html_url: string;
};
type IssueSearchResponse = {
items: IssueSearchItem[];
total_count: number;
};
export async function getIssues(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
let responseData: IssueSearchResponse = {
items: [],
total_count: 0,
};
const owner = this.getNodeParameter('owner', '', { extractValue: true });
const repository = this.getNodeParameter('repository', '', { extractValue: true });
const filters = [filter, `repo:${owner}/${repository}`];
responseData = await githubApiRequest.call(this, 'GET', '/search/issues', {
q: filters.filter(Boolean).join(' '),
page,
per_page,
});
const results: INodeListSearchItems[] = responseData.items.map((item: IssueSearchItem) => ({
name: item.title,
value: item.number,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}

View file

@ -0,0 +1,50 @@
import type {
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
import { githubApiRequest } from '../shared/transport';
type RepositorySearchItem = {
name: string;
html_url: string;
};
type RepositorySearchResponse = {
items: RepositorySearchItem[];
total_count: number;
};
export async function getRepositories(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const owner = this.getCurrentNodeParameter('owner', { extractValue: true });
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
const q = `${filter ?? ''} user:${owner} fork:true`;
let responseData: RepositorySearchResponse = {
items: [],
total_count: 0,
};
try {
responseData = await githubApiRequest.call(this, 'GET', '/search/repositories', {
q,
page,
per_page,
});
} catch {
// will fail if the owner does not have any repositories
}
const results: INodeListSearchItems[] = responseData.items.map((item: RepositorySearchItem) => ({
name: item.name,
value: item.name,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}

View file

@ -0,0 +1,49 @@
import type {
ILoadOptionsFunctions,
INodeListSearchResult,
INodeListSearchItems,
} from 'n8n-workflow';
import { githubApiRequest } from '../shared/transport';
type UserSearchItem = {
login: string;
html_url: string;
};
type UserSearchResponse = {
items: UserSearchItem[];
total_count: number;
};
export async function getUsers(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
let responseData: UserSearchResponse = {
items: [],
total_count: 0,
};
try {
responseData = await githubApiRequest.call(this, 'GET', '/search/users', {
q: filter,
page,
per_page,
});
} catch {
// will fail if the owner does not have any users
}
const results: INodeListSearchItems[] = responseData.items.map((item: UserSearchItem) => ({
name: item.login,
value: item.login,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}

View file

@ -0,0 +1,74 @@
import type { INodeProperties } from 'n8n-workflow';
const showOnlyForIssueCreate = {
operation: ['create'],
resource: ['issue'],
};
export const issueCreateDescription: INodeProperties[] = [
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
required: true,
displayOptions: {
show: showOnlyForIssueCreate,
},
description: 'The title of the issue',
routing: {
send: {
type: 'body',
property: 'title',
},
},
},
{
displayName: 'Body',
name: 'body',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
displayOptions: {
show: showOnlyForIssueCreate,
},
description: 'The body of the issue',
routing: {
send: {
type: 'body',
property: 'body',
},
},
},
{
displayName: 'Labels',
name: 'labels',
type: 'collection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Label',
},
displayOptions: {
show: showOnlyForIssueCreate,
},
default: { label: '' },
options: [
{
displayName: 'Label',
name: 'label',
type: 'string',
default: '',
description: 'Label to add to issue',
},
],
routing: {
send: {
type: 'body',
property: 'labels',
value: '={{$value.map((data) => data.label)}}',
},
},
},
];

View file

@ -0,0 +1,14 @@
import type { INodeProperties } from 'n8n-workflow';
import { issueSelect } from '../../shared/descriptions';
const showOnlyForIssueGet = {
operation: ['get'],
resource: ['issue'],
};
export const issueGetDescription: INodeProperties[] = [
{
...issueSelect,
displayOptions: { show: showOnlyForIssueGet },
},
];

View file

@ -0,0 +1,124 @@
import type { INodeProperties } from 'n8n-workflow';
import { parseLinkHeader } from '../../shared/utils';
const showOnlyForIssueGetMany = {
operation: ['getAll'],
resource: ['issue'],
};
export const issueGetManyDescription: INodeProperties[] = [
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
...showOnlyForIssueGetMany,
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
routing: {
send: {
type: 'query',
property: 'per_page',
},
output: {
maxResults: '={{$value}}',
},
},
description: 'Max number of results to return',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: showOnlyForIssueGetMany,
},
default: false,
description: 'Whether to return all results or only up to a given limit',
routing: {
send: {
paginate: '={{ $value }}',
type: 'query',
property: 'per_page',
value: '100',
},
operations: {
pagination: {
type: 'generic',
properties: {
continue: `={{ !!(${parseLinkHeader.toString()})($response.headers?.link).next }}`,
request: {
url: `={{ (${parseLinkHeader.toString()})($response.headers?.link)?.next ?? $request.url }}`,
},
},
},
},
},
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
typeOptions: {
multipleValueButtonText: 'Add Filter',
},
displayOptions: {
show: showOnlyForIssueGetMany,
},
default: {},
options: [
{
displayName: 'Updated Since',
name: 'since',
type: 'dateTime',
default: '',
description: 'Return only issues updated at or after this time',
routing: {
request: {
qs: {
since: '={{$value}}',
},
},
},
},
{
displayName: 'State',
name: 'state',
type: 'options',
options: [
{
name: 'All',
value: 'all',
description: 'Returns issues with any state',
},
{
name: 'Closed',
value: 'closed',
description: 'Return issues with "closed" state',
},
{
name: 'Open',
value: 'open',
description: 'Return issues with "open" state',
},
],
default: 'open',
description: 'The issue state to filter on',
routing: {
request: {
qs: {
state: '={{$value}}',
},
},
},
},
],
},
];

View file

@ -0,0 +1,75 @@
import type { INodeProperties } from 'n8n-workflow';
import { repoNameSelect, repoOwnerSelect } from '../../shared/descriptions';
import { issueGetManyDescription } from './getAll';
import { issueGetDescription } from './get';
import { issueCreateDescription } from './create';
const showOnlyForIssues = {
resource: ['issue'],
};
export const issueDescription: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: showOnlyForIssues,
},
options: [
{
name: 'Get Many',
value: 'getAll',
action: 'Get issues in a repository',
description: 'Get many issues in a repository',
routing: {
request: {
method: 'GET',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues',
},
},
},
{
name: 'Get',
value: 'get',
action: 'Get an issue',
description: 'Get the data of a single issue',
routing: {
request: {
method: 'GET',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues/{{$parameter.issue}}',
},
},
},
{
name: 'Create',
value: 'create',
action: 'Create a new issue',
description: 'Create a new issue',
routing: {
request: {
method: 'POST',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues',
},
},
},
],
default: 'getAll',
},
{
...repoOwnerSelect,
displayOptions: {
show: showOnlyForIssues,
},
},
{
...repoNameSelect,
displayOptions: {
show: showOnlyForIssues,
},
},
...issueGetManyDescription,
...issueGetDescription,
...issueCreateDescription,
];

View file

@ -0,0 +1,65 @@
import type { INodeProperties } from 'n8n-workflow';
import { parseLinkHeader } from '../../shared/utils';
const showOnlyForIssueCommentGetMany = {
operation: ['getAll'],
resource: ['issueComment'],
};
export const issueCommentGetManyDescription: INodeProperties[] = [
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
...showOnlyForIssueCommentGetMany,
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
routing: {
send: {
type: 'query',
property: 'per_page',
},
output: {
maxResults: '={{$value}}',
},
},
description: 'Max number of results to return',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: showOnlyForIssueCommentGetMany,
},
default: false,
description: 'Whether to return all results or only up to a given limit',
routing: {
send: {
paginate: '={{ $value }}',
type: 'query',
property: 'per_page',
value: '100',
},
operations: {
pagination: {
type: 'generic',
properties: {
continue: `={{ !!(${parseLinkHeader.toString()})($response.headers?.link).next }}`,
request: {
url: `={{ (${parseLinkHeader.toString()})($response.headers?.link)?.next ?? $request.url }}`,
},
},
},
},
},
},
];

View file

@ -0,0 +1,47 @@
import type { INodeProperties } from 'n8n-workflow';
import { repoNameSelect, repoOwnerSelect } from '../../shared/descriptions';
import { issueCommentGetManyDescription } from './getAll';
const showOnlyForIssueComments = {
resource: ['issueComment'],
};
export const issueCommentDescription: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: showOnlyForIssueComments,
},
options: [
{
name: 'Get Many',
value: 'getAll',
action: 'Get issue comments',
description: 'Get issue comments',
routing: {
request: {
method: 'GET',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues/comments',
},
},
},
],
default: 'getAll',
},
{
...repoOwnerSelect,
displayOptions: {
show: showOnlyForIssueComments,
},
},
{
...repoNameSelect,
displayOptions: {
show: showOnlyForIssueComments,
},
},
...issueCommentGetManyDescription,
];

View file

@ -0,0 +1,151 @@
import type { INodeProperties } from 'n8n-workflow';
export const repoOwnerSelect: INodeProperties = {
displayName: 'Repository Owner',
name: 'owner',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'Repository Owner',
name: 'list',
type: 'list',
placeholder: 'Select an owner...',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
searchFilterRequired: false,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://github.com/n8n-io',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid GitHub URL',
},
},
],
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
placeholder: 'e.g. n8n-io',
validation: [
{
type: 'regex',
properties: {
regex: '[-_a-zA-Z0-9]+',
errorMessage: 'Not a valid GitHub Owner Name',
},
},
],
url: '=https://github.com/{{$value}}',
},
],
};
export const repoNameSelect: INodeProperties = {
displayName: 'Repository Name',
name: 'repository',
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
required: true,
modes: [
{
displayName: 'Repository Name',
name: 'list',
type: 'list',
placeholder: 'Select an Repository...',
typeOptions: {
searchListMethod: 'getRepositories',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://github.com/n8n-io/n8n',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid GitHub Repository URL',
},
},
],
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
placeholder: 'e.g. n8n',
validation: [
{
type: 'regex',
properties: {
regex: '[-_.0-9a-zA-Z]+',
errorMessage: 'Not a valid GitHub Repository Name',
},
},
],
url: '=https://github.com/{{$parameter["owner"]}}/{{$value}}',
},
],
displayOptions: {
hide: {
resource: ['user', 'organization'],
operation: ['getRepositories'],
},
},
};
export const issueSelect: INodeProperties = {
displayName: 'Issue',
name: 'issue',
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
required: true,
modes: [
{
displayName: 'Issue',
name: 'list',
type: 'list',
placeholder: 'Select an Issue...',
typeOptions: {
searchListMethod: 'getIssues',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'name',
type: 'string',
placeholder: 'e.g. 123',
url: '=https://github.com/{{$parameter.owner}}/{{$parameter.repository}}/issues/{{$value}}',
},
],
};

View file

@ -0,0 +1,32 @@
import type {
IHookFunctions,
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IHttpRequestMethods,
IDataObject,
IHttpRequestOptions,
} from 'n8n-workflow';
export async function githubApiRequest(
this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
qs: IDataObject = {},
body: IDataObject | undefined = undefined,
) {
const authenticationMethod = this.getNodeParameter('authentication', 0);
const options: IHttpRequestOptions = {
method: method,
qs,
body,
url: `https://api.github.com${resource}`,
json: true,
};
const credentialType =
authenticationMethod === 'accessToken' ? 'githubIssuesApi' : 'githubIssuesOAuth2Api';
return this.helpers.httpRequestWithAuthentication.call(this, credentialType, options);
}

View file

@ -0,0 +1,14 @@
export function parseLinkHeader(header?: string): { [rel: string]: string } {
const links: { [rel: string]: string } = {};
for (const part of header?.split(',') ?? []) {
const section = part.trim();
const match = section.match(/^<([^>]+)>\s*;\s*rel="?([^"]+)"?/);
if (match) {
const [, url, rel] = match;
links[rel] = url;
}
}
return links;
}

View file

@ -1,63 +0,0 @@
import { INodeType, INodeTypeDescription, NodeConnectionType } from 'n8n-workflow';
import { httpVerbFields, httpVerbOperations } from './HttpVerbDescription';
export class HttpBin implements INodeType {
description: INodeTypeDescription = {
displayName: 'HttpBin',
name: 'httpBin',
icon: { light: 'file:httpbin.svg', dark: 'file:httpbin.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with HttpBin API',
defaults: {
name: 'HttpBin',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
usableAsTool: true,
credentials: [
{
name: 'httpbinApi',
required: false,
},
],
requestDefaults: {
baseURL: 'https://httpbin.org',
url: '',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
/**
* In the properties array we have two mandatory options objects required
*
* [Resource & Operation]
*
* https://docs.n8n.io/integrations/creating-nodes/code/create-first-node/#resources-and-operations
*
* In our example, the operations are separated into their own file (HTTPVerbDescription.ts)
* to keep this class easy to read.
*
*/
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'HTTP Verb',
value: 'httpVerb',
},
],
default: 'httpVerb',
},
...httpVerbOperations,
...httpVerbFields,
],
};
}

View file

@ -1,250 +0,0 @@
import { INodeProperties } from 'n8n-workflow';
// When the resource `httpVerb` is selected, this `operation` parameter will be shown.
export const httpVerbOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['httpVerb'],
},
},
options: [
{
name: 'GET',
value: 'get',
description: 'Perform a GET request',
action: 'Perform a GET request',
routing: {
request: {
method: 'GET',
url: '/get',
},
},
},
{
name: 'DELETE',
value: 'delete',
description: 'Perform a DELETE request',
action: 'Perform a DELETE request',
routing: {
request: {
method: 'DELETE',
url: '/delete',
},
},
},
],
default: 'get',
},
];
// Here we define what to show when the `get` operation is selected.
// We do that by adding `operation: ["get"]` to `displayOptions.show`
const getOperation: INodeProperties[] = [
{
displayName: 'Type of Data',
name: 'typeofData',
default: 'queryParameter',
description: 'Select type of data to send [Query Parameters]',
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['get'],
},
},
type: 'options',
options: [
{
name: 'Query',
value: 'queryParameter',
},
],
required: true,
},
{
displayName: 'Query Parameters',
name: 'arguments',
default: {},
description: "The request's query parameters",
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['get'],
},
},
options: [
{
name: 'keyvalue',
displayName: 'Key:Value',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
required: true,
description: 'Key of query parameter',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '={{$parent.key}}',
type: 'query',
},
},
required: true,
description: 'Value of query parameter',
},
],
},
],
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
];
// Here we define what to show when the DELETE Operation is selected.
// We do that by adding `operation: ["delete"]` to `displayOptions.show`
const deleteOperation: INodeProperties[] = [
{
displayName: 'Type of Data',
name: 'typeofData',
default: 'queryParameter',
description: 'Select type of data to send [Query Parameter Arguments, JSON-Body]',
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['delete'],
},
},
options: [
{
name: 'Query',
value: 'queryParameter',
},
{
name: 'JSON',
value: 'jsonData',
},
],
required: true,
type: 'options',
},
{
displayName: 'Query Parameters',
name: 'arguments',
default: {},
description: "The request's query parameters",
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['delete'],
typeofData: ['queryParameter'],
},
},
options: [
{
name: 'keyvalue',
displayName: 'Key:Value',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
required: true,
description: 'Key of query parameter',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '={{$parent.key}}',
type: 'query',
},
},
required: true,
description: 'Value of query parameter',
},
],
},
],
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
{
displayName: 'JSON Object',
name: 'arguments',
default: {},
description: "The request's JSON properties",
displayOptions: {
show: {
resource: ['httpVerb'],
operation: ['delete'],
typeofData: ['jsonData'],
},
},
options: [
{
name: 'keyvalue',
displayName: 'Key:Value',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
required: true,
description: 'Key of JSON property',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
routing: {
send: {
property: '={{$parent.key}}',
type: 'body',
},
},
required: true,
description: 'Value of JSON property',
},
],
},
],
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
];
export const httpVerbFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* httpVerb:get */
/* -------------------------------------------------------------------------- */
...getOperation,
/* -------------------------------------------------------------------------- */
/* httpVerb:delete */
/* -------------------------------------------------------------------------- */
...deleteOperation,
];

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve"> <image id="image0" width="32" height="32" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElN
RQfmBg4UAC/TqOZZAAACA0lEQVRIx5XVv09TURwF8M+jFHDSyRkGFhPAEfyRdDHi5uriXyDoYgKT
MJDWzUT/Ahf/AiOEpajEgCESmpiYmDCxGowDTYE+h76+vte+15Zzk753b7733HNO772PbEw7ECba
genswtEcgl0/PHARV72066YrIDSZ6k8KBym4741r0XsB284TdUX8chn1zrzwJUmw4KFXPqjFE0Y0
u5YKEhpmfLZuy7f2wLKGI8WhDRYdaVhurdTCidmU5P44N+skaaGQH1IfFFrOYMotT932zNgQExve
OfTeT8dtBceO3TFlOyopY7UPxV+/fWyn3Y0xrFhJjZWFXhs12pKdRO9ObGSuyB8Xbd9JjMjDc6HQ
IcrKqAiVe8vyCEJPrGBWxZYqqtZt9RbmHabAvAAVdVUlJTvWshbMt0AYn40OmlchSKOePTyYIMQn
rb8yI8TsDCrRs4od7Jv3KOoPGWKboBqp2LN3FQvdO7EPshSsRSTXrSop2cSiiUGkG/bj2JqaQiHW
4nv50mFcu28j30KQarAnEPhuzvwwGYQ975vx7+JwGXTjTIAzoYlhCArR5d0KkfauqJAVY6+FG5hD
OS6veqyCuSiTAQT/jKmlQtyxIBCoZV28HQvN6LuQvJFC4xjvibfYOZUdUXd9taTWJbOubiIVXmjG
W/fs9qpZcpr6pOe1U0udSf8BR7ef4yxyOskAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjItMDYtMTRU
MTc6MDA6NDcrMDM6MDBfo1sRAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIyLTA2LTE0VDE3OjAwOjQ3
KzAzOjAwLv7jrQAAAABJRU5ErkJggg==" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

7226
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,55 +1,52 @@
{ {
"name": "n8n-nodes-<...>", "name": "n8n-nodes-<...>",
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"keywords": [ "license": "MIT",
"n8n-community-node-package" "homepage": "",
], "keywords": [
"license": "MIT", "n8n-community-node-package"
"homepage": "", ],
"author": { "author": {
"name": "", "name": "Elias Meire",
"email": "" "email": "elias@meire.dev"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/<...>/n8n-nodes-<...>.git" "url": "https://github.com/<...>/n8n-nodes-<...>.git"
}, },
"engines": { "scripts": {
"node": ">=20.15" "build": "n8n-node build",
}, "build:watch": "tsc --watch",
"main": "index.js", "dev": "n8n-node dev",
"scripts": { "lint": "n8n-node lint",
"build": "npx rimraf dist && tsc && gulp build:icons", "lint:fix": "n8n-node lint --fix",
"dev": "tsc --watch", "release": "n8n-node release",
"format": "prettier nodes credentials --write", "prepublishOnly": "n8n-node prerelease"
"lint": "eslint nodes credentials package.json", },
"lintfix": "eslint nodes credentials package.json --fix", "files": [
"prepublishOnly": "npm run build && npm run lint -c .eslintrc.prepublish.js nodes credentials package.json" "dist"
}, ],
"files": [ "n8n": {
"dist" "n8nNodesApiVersion": 1,
], "strict": true,
"n8n": { "credentials": [
"n8nNodesApiVersion": 1, "dist/credentials/GithubIssuesApi.credentials.js",
"credentials": [ "dist/credentials/GithubIssuesOAuth2Api.credentials.js"
"dist/credentials/ExampleCredentialsApi.credentials.js", ],
"dist/credentials/HttpBinApi.credentials.js" "nodes": [
], "dist/nodes/GithubIssues/GithubIssues.node.js",
"nodes": [ "dist/nodes/Example/Example.node.js"
"dist/nodes/ExampleNode/ExampleNode.node.js", ]
"dist/nodes/HttpBin/HttpBin.node.js" },
] "devDependencies": {
}, "@n8n/node-cli": "*",
"devDependencies": { "eslint": "9.32.0",
"@typescript-eslint/parser": "~8.32.0", "prettier": "3.6.2",
"eslint": "^8.57.0", "release-it": "^19.0.4",
"eslint-plugin-n8n-nodes-base": "^1.16.3", "typescript": "5.9.2"
"gulp": "^5.0.0", },
"prettier": "^3.5.3", "peerDependencies": {
"typescript": "^5.8.2" "n8n-workflow": "*"
}, }
"peerDependencies": { }
"n8n-workflow": "*"
}
}

View file

@ -19,12 +19,7 @@
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"skipLibCheck": true, "skipLibCheck": true,
"outDir": "./dist/", "outDir": "./dist/"
}, },
"include": [ "include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"]
"credentials/**/*",
"nodes/**/*",
"nodes/**/*.json",
"package.json",
],
} }