mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat: Create NPM node (#6177)
This commit is contained in:
parent
80831cd7c6
commit
f3bc6f19b6
|
@ -791,7 +791,7 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
|
|||
// if there is a body and it's empty (does not have properties),
|
||||
// make sure not to send anything in it as some services fail when
|
||||
// sending GET request with empty body.
|
||||
if (typeof body === 'object' && !isObjectEmpty(body)) {
|
||||
if (typeof body === 'string' || (typeof body === 'object' && !isObjectEmpty(body))) {
|
||||
axiosRequest.data = body;
|
||||
}
|
||||
}
|
||||
|
|
46
packages/nodes-base/credentials/NpmApi.credentials.ts
Normal file
46
packages/nodes-base/credentials/NpmApi.credentials.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import type {
|
||||
IAuthenticateGeneric,
|
||||
ICredentialTestRequest,
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class NpmApi implements ICredentialType {
|
||||
name = 'npmApi';
|
||||
|
||||
displayName = 'Npm API';
|
||||
|
||||
documentationUrl = 'npm';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string',
|
||||
typeOptions: { password: true },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Registry Url',
|
||||
name: 'registryUrl',
|
||||
type: 'string',
|
||||
default: 'https://registry.npmjs.org',
|
||||
},
|
||||
];
|
||||
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
headers: {
|
||||
Authorization: '=Bearer {{$credentials.accessToken}}',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test: ICredentialTestRequest = {
|
||||
request: {
|
||||
baseURL: '={{$credentials.registryUrl}}',
|
||||
url: '/-/whoami',
|
||||
},
|
||||
};
|
||||
}
|
94
packages/nodes-base/nodes/Npm/DistTagDescription.ts
Normal file
94
packages/nodes-base/nodes/Npm/DistTagDescription.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const distTagOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'getMany',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['distTag'],
|
||||
},
|
||||
},
|
||||
|
||||
options: [
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getMany',
|
||||
action: 'Returns all the dist-tags for a package',
|
||||
description: 'Returns all the dist-tags for a package',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/-/package/{{ encodeURIComponent($parameter.packageName) }}/dist-tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
action: 'Update a the dist-tags for a package',
|
||||
description: 'Update a the dist-tags for a package',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/-/package/{{ encodeURIComponent($parameter.packageName) }}/dist-tags/{{ encodeURIComponent($parameter.distTagName) }}',
|
||||
},
|
||||
send: {
|
||||
preSend: [
|
||||
async function (this, requestOptions) {
|
||||
requestOptions.headers!['content-type'] = 'application/x-www-form-urlencoded';
|
||||
requestOptions.body = this.getNodeParameter('packageVersion');
|
||||
return requestOptions;
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const distTagFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Package Name',
|
||||
name: 'packageName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['distTag'],
|
||||
operation: ['getMany', 'update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Package Version',
|
||||
name: 'packageVersion',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['distTag'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Distribution Tag Name',
|
||||
name: 'distTagName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'latest',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['distTag'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
19
packages/nodes-base/nodes/Npm/Npm.node.json
Normal file
19
packages/nodes-base/nodes/Npm/Npm.node.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.npm",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Development"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/credentials/npm"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.npm/"
|
||||
}
|
||||
],
|
||||
"generic": []
|
||||
}
|
||||
}
|
54
packages/nodes-base/nodes/Npm/Npm.node.ts
Normal file
54
packages/nodes-base/nodes/Npm/Npm.node.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { packageFields, packageOperations } from './PackageDescription';
|
||||
import { distTagFields, distTagOperations } from './DistTagDescription';
|
||||
|
||||
export class Npm implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Npm',
|
||||
name: 'npm',
|
||||
icon: 'file:npm.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
|
||||
description: 'Consume NPM registry API',
|
||||
defaults: {
|
||||
name: 'npm',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'npmApi',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
baseURL: '={{ $credentials.registryUrl }}',
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Package',
|
||||
value: 'package',
|
||||
},
|
||||
{
|
||||
name: 'Distribution Tag',
|
||||
value: 'distTag',
|
||||
},
|
||||
],
|
||||
default: 'package',
|
||||
},
|
||||
|
||||
...packageOperations,
|
||||
...packageFields,
|
||||
|
||||
...distTagOperations,
|
||||
...distTagFields,
|
||||
],
|
||||
};
|
||||
}
|
181
packages/nodes-base/nodes/Npm/PackageDescription.ts
Normal file
181
packages/nodes-base/nodes/Npm/PackageDescription.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
import { valid as isValidSemver } from 'semver';
|
||||
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
interface PackageJson {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const packageOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'getMetadata',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['package'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get Metadata',
|
||||
value: 'getMetadata',
|
||||
action: 'Returns all the metadata for a package at a specific version',
|
||||
description: 'Returns all the metadata for a package at a specific version',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/{{ encodeURIComponent($parameter.packageName) }}/{{ $parameter.packageVersion }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Get Versions',
|
||||
value: 'getVersions',
|
||||
action: 'Returns all the versions for a package',
|
||||
description: 'Returns all the versions for a package',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/{{ encodeURIComponent($parameter.packageName) }}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
async function (items) {
|
||||
const allVersions: INodeExecutionData[] = [];
|
||||
for (const { json } of items) {
|
||||
const itemVersions = json.time as Record<string, string>;
|
||||
Object.keys(itemVersions).forEach((version) => {
|
||||
if (isValidSemver(version)) {
|
||||
allVersions.push({
|
||||
json: {
|
||||
version,
|
||||
published_at: itemVersions[version],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
allVersions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.json.published_at as string).getTime() -
|
||||
new Date(a.json.published_at as string).getTime(),
|
||||
);
|
||||
return allVersions;
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
action: 'Search for packages',
|
||||
description: 'Search for packages',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/-/v1/search',
|
||||
qs: {
|
||||
text: '={{$parameter.query}}',
|
||||
size: '={{$parameter.limit}}',
|
||||
from: '={{$parameter.offset}}',
|
||||
popularity: 0.99,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
async function (items) {
|
||||
return items.flatMap(({ json }) =>
|
||||
(json.objects as Array<{ package: PackageJson }>).map(
|
||||
({ package: { name, version, description } }) =>
|
||||
({ json: { name, version, description } } as INodeExecutionData),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const packageFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Package Name',
|
||||
name: 'packageName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['package'],
|
||||
operation: ['getMetadata', 'getVersions'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Package Version',
|
||||
name: 'packageVersion',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'latest',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['package'],
|
||||
operation: ['getMetadata'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['package'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'The query text used to search for packages',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 10,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['package'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Offset',
|
||||
name: 'offset',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['package'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
description: 'Offset to return results from',
|
||||
},
|
||||
];
|
4
packages/nodes-base/nodes/Npm/npm.svg
Normal file
4
packages/nodes-base/nodes/Npm/npm.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2500 2500">
|
||||
<path d="M0 0h2500v2500H0z" fill="#c00"/>
|
||||
<path d="M1241.5 268.5h-973v1962.9h972.9V763.5h495v1467.9h495V268.5z" fill="#fff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 202 B |
38
packages/nodes-base/nodes/Npm/test/Npm.node.test.ts
Normal file
38
packages/nodes-base/nodes/Npm/test/Npm.node.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import nock from 'nock';
|
||||
import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
|
||||
import { FAKE_CREDENTIALS_DATA } from '@test/nodes/FakeCredentialsMap';
|
||||
|
||||
describe('Test npm Node', () => {
|
||||
beforeAll(() => {
|
||||
nock.disableNetConnect();
|
||||
|
||||
const { registryUrl } = FAKE_CREDENTIALS_DATA.npmApi;
|
||||
const mock = nock(registryUrl); //.matchHeader('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
mock.get('/-/package/n8n/dist-tags').reply(200, {
|
||||
latest: '0.225.2',
|
||||
next: '0.226.2',
|
||||
});
|
||||
|
||||
mock.get('/n8n').reply(200, {
|
||||
time: {
|
||||
'0.225.2': '2023-04-25T09:45:36.407Z',
|
||||
'0.226.2': '2023-05-03T09:41:30.844Z',
|
||||
'0.227.0': '2023-05-03T13:44:32.079Z',
|
||||
},
|
||||
});
|
||||
|
||||
mock.get('/n8n/latest').reply(200, {
|
||||
name: 'n8n',
|
||||
version: '0.225.2',
|
||||
rest: 'of the properties',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.restore();
|
||||
});
|
||||
|
||||
const workflows = getWorkflowFilenames(__dirname);
|
||||
testWorkflows(workflows);
|
||||
});
|
117
packages/nodes-base/nodes/Npm/test/Npm.workflow.test.json
Normal file
117
packages/nodes-base/nodes/Npm/test/Npm.workflow.test.json
Normal file
|
@ -0,0 +1,117 @@
|
|||
{
|
||||
"name": "Test NPM",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "Execute Workflow",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [620, 440]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "distTag",
|
||||
"operation": "getMany",
|
||||
"packageName": "n8n"
|
||||
},
|
||||
"name": "Get All dist-tags",
|
||||
"type": "n8n-nodes-base.npm",
|
||||
"credentials": {
|
||||
"npmApi": {
|
||||
"id": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "package",
|
||||
"operation": "getVersions",
|
||||
"packageName": "n8n"
|
||||
},
|
||||
"name": "Get Versions",
|
||||
"type": "n8n-nodes-base.npm",
|
||||
"credentials": {
|
||||
"npmApi": {
|
||||
"id": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "package",
|
||||
"operation": "getMetadata",
|
||||
"packageName": "n8n",
|
||||
"packageVersion": "latest"
|
||||
},
|
||||
"name": "Get Metadata",
|
||||
"type": "n8n-nodes-base.npm",
|
||||
"credentials": {
|
||||
"npmApi": {
|
||||
"id": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get All dist-tags": [
|
||||
{
|
||||
"json": {
|
||||
"latest": "0.225.2",
|
||||
"next": "0.226.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Get Versions": [
|
||||
{
|
||||
"json": {
|
||||
"version": "0.227.0",
|
||||
"published_at": "2023-05-03T13:44:32.079Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"version": "0.226.2",
|
||||
"published_at": "2023-05-03T09:41:30.844Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"version": "0.225.2",
|
||||
"published_at": "2023-04-25T09:45:36.407Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Get Metadata": [
|
||||
{
|
||||
"json": {
|
||||
"name": "n8n",
|
||||
"version": "0.225.2",
|
||||
"rest": "of the properties"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"Execute Workflow": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get All dist-tags",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Get Versions",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Get Metadata",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -223,6 +223,7 @@
|
|||
"dist/credentials/NocoDbApiToken.credentials.js",
|
||||
"dist/credentials/NotionApi.credentials.js",
|
||||
"dist/credentials/NotionOAuth2Api.credentials.js",
|
||||
"dist/credentials/NpmApi.credentials.js",
|
||||
"dist/credentials/OAuth1Api.credentials.js",
|
||||
"dist/credentials/OAuth2Api.credentials.js",
|
||||
"dist/credentials/OdooApi.credentials.js",
|
||||
|
@ -596,6 +597,7 @@
|
|||
"dist/nodes/Onfleet/OnfleetTrigger.node.js",
|
||||
"dist/nodes/Notion/Notion.node.js",
|
||||
"dist/nodes/Notion/NotionTrigger.node.js",
|
||||
"dist/nodes/Npm/Npm.node.js",
|
||||
"dist/nodes/Odoo/Odoo.node.js",
|
||||
"dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
|
||||
"dist/nodes/OpenAi/OpenAi.node.js",
|
||||
|
@ -896,6 +898,7 @@
|
|||
"request": "^2.88.2",
|
||||
"rhea": "^1.0.11",
|
||||
"rss-parser": "^3.7.0",
|
||||
"semver": "^7.3.8",
|
||||
"showdown": "^2.0.3",
|
||||
"simple-git": "^3.17.0",
|
||||
"snowflake-sdk": "^1.5.3",
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
// If your test needs data from credentials, you can add it here.
|
||||
// as JSON.stringify({ id: 'credentials_ID', name: 'credentials_name' }) for specific credentials
|
||||
// or as 'credentials_type' for all credentials of that type
|
||||
// expected keys for credentials can be found in packages/nodes-base/credentials/[credentials_type].credentials.ts
|
||||
export const FAKE_CREDENTIALS_DATA: IDataObject = {
|
||||
export const FAKE_CREDENTIALS_DATA = {
|
||||
[JSON.stringify({ id: '20', name: 'Airtable account' })]: {
|
||||
apiKey: 'key456',
|
||||
},
|
||||
|
@ -15,6 +13,10 @@ export const FAKE_CREDENTIALS_DATA: IDataObject = {
|
|||
apiKey: 'key123',
|
||||
baseUrl: 'https://test.app.n8n.cloud/api/v1',
|
||||
},
|
||||
npmApi: {
|
||||
accessToken: 'fake-npm-access-token',
|
||||
registryUrl: 'https://fake.npm.registry',
|
||||
},
|
||||
totpApi: {
|
||||
label: 'GitHub:john-doe',
|
||||
secret: 'BVDRSBXQB2ZEL5HE',
|
||||
|
@ -24,4 +26,4 @@ export const FAKE_CREDENTIALS_DATA: IDataObject = {
|
|||
accessKeyId: 'key',
|
||||
secretAccessKey: 'secret',
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
|
|
@ -1417,6 +1417,9 @@ importers:
|
|||
rss-parser:
|
||||
specifier: ^3.7.0
|
||||
version: 3.12.0
|
||||
semver:
|
||||
specifier: ^7.3.8
|
||||
version: 7.3.8
|
||||
showdown:
|
||||
specifier: ^2.0.3
|
||||
version: 2.1.0
|
||||
|
|
Loading…
Reference in a new issue