feat: Create NPM node (#6177)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-05-10 10:37:26 +00:00 committed by GitHub
parent 80831cd7c6
commit f3bc6f19b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 566 additions and 5 deletions

View file

@ -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;
}
}

View 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',
},
};
}

View 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'],
},
},
},
];

View 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": []
}
}

View 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,
],
};
}

View 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',
},
];

View 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

View 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);
});

View 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
}
]
]
}
}
}

View file

@ -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",

View file

@ -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;

View file

@ -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