@n8n/eslint-plugin-community-nodes 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/README.md +30 -24
- package/dist/plugin.d.ts +24 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +8 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +4 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +8 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-overrides-field.d.ts +2 -0
- package/dist/rules/no-overrides-field.d.ts.map +1 -0
- package/dist/rules/no-overrides-field.js +37 -0
- package/dist/rules/no-overrides-field.js.map +1 -0
- package/dist/rules/require-node-api-error.d.ts +2 -0
- package/dist/rules/require-node-api-error.d.ts.map +1 -0
- package/dist/rules/require-node-api-error.js +77 -0
- package/dist/rules/require-node-api-error.js.map +1 -0
- package/dist/rules/valid-peer-dependencies.d.ts +2 -0
- package/dist/rules/valid-peer-dependencies.d.ts.map +1 -0
- package/dist/rules/valid-peer-dependencies.js +107 -0
- package/dist/rules/valid-peer-dependencies.js.map +1 -0
- package/dist/rules/webhook-lifecycle-complete.d.ts +2 -0
- package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -0
- package/dist/rules/webhook-lifecycle-complete.js +98 -0
- package/dist/rules/webhook-lifecycle-complete.js.map +1 -0
- package/docs/rules/no-overrides-field.md +50 -0
- package/docs/rules/node-class-description-icon-missing.md +4 -2
- package/docs/rules/require-community-node-keyword.md +3 -3
- package/docs/rules/require-node-api-error.md +62 -0
- package/docs/rules/require-node-description-fields.md +1 -1
- package/docs/rules/valid-peer-dependencies.md +72 -0
- package/docs/rules/webhook-lifecycle-complete.md +88 -0
- package/package.json +5 -5
- package/src/plugin.ts +8 -0
- package/src/rules/index.ts +8 -0
- package/src/rules/no-overrides-field.test.ts +50 -0
- package/src/rules/no-overrides-field.ts +43 -0
- package/src/rules/require-node-api-error.test.ts +199 -0
- package/src/rules/require-node-api-error.ts +90 -0
- package/src/rules/valid-peer-dependencies.test.ts +130 -0
- package/src/rules/valid-peer-dependencies.ts +116 -0
- package/src/rules/webhook-lifecycle-complete.test.ts +212 -0
- package/src/rules/webhook-lifecycle-complete.ts +120 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.json +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { WebhookLifecycleCompleteRule } from './webhook-lifecycle-complete.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function createTriggerNode(options: {
|
|
8
|
+
group?: string;
|
|
9
|
+
hasWebhooks?: boolean;
|
|
10
|
+
webhookMethods?: string | null;
|
|
11
|
+
}): string {
|
|
12
|
+
const { group = 'trigger', hasWebhooks = true, webhookMethods } = options;
|
|
13
|
+
const webhooksProp = hasWebhooks
|
|
14
|
+
? "webhooks: [{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'webhook' }],"
|
|
15
|
+
: '';
|
|
16
|
+
const methodsProp = webhookMethods === null ? '' : `\n\twebhookMethods = ${webhookMethods};`;
|
|
17
|
+
|
|
18
|
+
return `
|
|
19
|
+
import type { INodeType, INodeTypeDescription, IHookFunctions } from 'n8n-workflow';
|
|
20
|
+
|
|
21
|
+
export class TestTrigger implements INodeType {
|
|
22
|
+
description: INodeTypeDescription = {
|
|
23
|
+
displayName: 'Test Trigger',
|
|
24
|
+
name: 'testTrigger',
|
|
25
|
+
group: ['${group}'],
|
|
26
|
+
version: 1,
|
|
27
|
+
description: 'A test trigger',
|
|
28
|
+
defaults: { name: 'Test Trigger' },
|
|
29
|
+
inputs: [],
|
|
30
|
+
outputs: ['main'],
|
|
31
|
+
${webhooksProp}
|
|
32
|
+
properties: [],
|
|
33
|
+
};${methodsProp}
|
|
34
|
+
}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const completeWebhookMethods = `{
|
|
38
|
+
default: {
|
|
39
|
+
async checkExists(this: IHookFunctions): Promise<boolean> { return true; },
|
|
40
|
+
async create(this: IHookFunctions): Promise<boolean> { return true; },
|
|
41
|
+
async delete(this: IHookFunctions): Promise<boolean> { return true; },
|
|
42
|
+
},
|
|
43
|
+
}`;
|
|
44
|
+
|
|
45
|
+
ruleTester.run('webhook-lifecycle-complete', WebhookLifecycleCompleteRule, {
|
|
46
|
+
valid: [
|
|
47
|
+
{
|
|
48
|
+
name: 'trigger node with all three webhook lifecycle methods',
|
|
49
|
+
code: createTriggerNode({ webhookMethods: completeWebhookMethods }),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'non-trigger node without webhookMethods',
|
|
53
|
+
code: createTriggerNode({
|
|
54
|
+
group: 'transform',
|
|
55
|
+
hasWebhooks: false,
|
|
56
|
+
webhookMethods: null,
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'polling trigger without webhooks array and without webhookMethods',
|
|
61
|
+
code: createTriggerNode({ hasWebhooks: false, webhookMethods: null }),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'non-INodeType class with webhookMethods (should be ignored)',
|
|
65
|
+
code: `
|
|
66
|
+
export class RegularClass {
|
|
67
|
+
webhookMethods = {
|
|
68
|
+
default: {
|
|
69
|
+
async checkExists() { return true; },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'trigger with multiple webhook groups, all complete',
|
|
76
|
+
code: createTriggerNode({
|
|
77
|
+
webhookMethods: `{
|
|
78
|
+
default: {
|
|
79
|
+
async checkExists() { return true; },
|
|
80
|
+
async create() { return true; },
|
|
81
|
+
async delete() { return true; },
|
|
82
|
+
},
|
|
83
|
+
setup: {
|
|
84
|
+
async checkExists() { return true; },
|
|
85
|
+
async create() { return true; },
|
|
86
|
+
async delete() { return true; },
|
|
87
|
+
},
|
|
88
|
+
}`,
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'webhook lifecycle defined via arrow functions',
|
|
93
|
+
code: createTriggerNode({
|
|
94
|
+
webhookMethods: `{
|
|
95
|
+
default: {
|
|
96
|
+
checkExists: async () => true,
|
|
97
|
+
create: async () => true,
|
|
98
|
+
delete: async () => true,
|
|
99
|
+
},
|
|
100
|
+
}`,
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
invalid: [
|
|
105
|
+
{
|
|
106
|
+
name: 'trigger node missing webhookMethods entirely',
|
|
107
|
+
code: createTriggerNode({ webhookMethods: null }),
|
|
108
|
+
errors: [{ messageId: 'missingWebhookMethods' }],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'trigger node with empty webhookMethods group (all three missing)',
|
|
112
|
+
code: createTriggerNode({
|
|
113
|
+
webhookMethods: `{
|
|
114
|
+
default: {},
|
|
115
|
+
}`,
|
|
116
|
+
}),
|
|
117
|
+
errors: [
|
|
118
|
+
{
|
|
119
|
+
messageId: 'missingLifecycleMethod',
|
|
120
|
+
data: {
|
|
121
|
+
group: 'default',
|
|
122
|
+
missing: '`checkExists`, `create`, `delete`',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'trigger node missing only delete',
|
|
129
|
+
code: createTriggerNode({
|
|
130
|
+
webhookMethods: `{
|
|
131
|
+
default: {
|
|
132
|
+
async checkExists() { return true; },
|
|
133
|
+
async create() { return true; },
|
|
134
|
+
},
|
|
135
|
+
}`,
|
|
136
|
+
}),
|
|
137
|
+
errors: [
|
|
138
|
+
{
|
|
139
|
+
messageId: 'missingLifecycleMethod',
|
|
140
|
+
data: { group: 'default', missing: '`delete`' },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'trigger node missing checkExists and create',
|
|
146
|
+
code: createTriggerNode({
|
|
147
|
+
webhookMethods: `{
|
|
148
|
+
default: {
|
|
149
|
+
async delete() { return true; },
|
|
150
|
+
},
|
|
151
|
+
}`,
|
|
152
|
+
}),
|
|
153
|
+
errors: [
|
|
154
|
+
{
|
|
155
|
+
messageId: 'missingLifecycleMethod',
|
|
156
|
+
data: { group: 'default', missing: '`checkExists`, `create`' },
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'non-trigger node with incomplete webhookMethods (still flagged)',
|
|
162
|
+
code: createTriggerNode({
|
|
163
|
+
group: 'transform',
|
|
164
|
+
hasWebhooks: false,
|
|
165
|
+
webhookMethods: `{
|
|
166
|
+
default: {
|
|
167
|
+
async create() { return true; },
|
|
168
|
+
},
|
|
169
|
+
}`,
|
|
170
|
+
}),
|
|
171
|
+
errors: [
|
|
172
|
+
{
|
|
173
|
+
messageId: 'missingLifecycleMethod',
|
|
174
|
+
data: { group: 'default', missing: '`checkExists`, `delete`' },
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'webhook trigger (detected via webhooks array) missing webhookMethods',
|
|
180
|
+
code: createTriggerNode({
|
|
181
|
+
group: 'transform',
|
|
182
|
+
hasWebhooks: true,
|
|
183
|
+
webhookMethods: null,
|
|
184
|
+
}),
|
|
185
|
+
errors: [{ messageId: 'missingWebhookMethods' }],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'multiple webhook groups each missing methods',
|
|
189
|
+
code: createTriggerNode({
|
|
190
|
+
webhookMethods: `{
|
|
191
|
+
default: {
|
|
192
|
+
async checkExists() { return true; },
|
|
193
|
+
},
|
|
194
|
+
setup: {
|
|
195
|
+
async create() { return true; },
|
|
196
|
+
async delete() { return true; },
|
|
197
|
+
},
|
|
198
|
+
}`,
|
|
199
|
+
}),
|
|
200
|
+
errors: [
|
|
201
|
+
{
|
|
202
|
+
messageId: 'missingLifecycleMethod',
|
|
203
|
+
data: { group: 'default', missing: '`create`, `delete`' },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
messageId: 'missingLifecycleMethod',
|
|
207
|
+
data: { group: 'setup', missing: '`checkExists`' },
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createRule,
|
|
5
|
+
findClassProperty,
|
|
6
|
+
findObjectProperty,
|
|
7
|
+
isNodeTypeClass,
|
|
8
|
+
} from '../utils/index.js';
|
|
9
|
+
|
|
10
|
+
const REQUIRED_METHODS = ['checkExists', 'create', 'delete'] as const;
|
|
11
|
+
type RequiredMethod = (typeof REQUIRED_METHODS)[number];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if the description declares webhook endpoints, indicating the
|
|
15
|
+
* node is a webhook-based trigger that needs a complete lifecycle.
|
|
16
|
+
*
|
|
17
|
+
* Polling triggers (group `['trigger']` without a `webhooks` array) do not
|
|
18
|
+
* register remote webhooks and are intentionally out of scope.
|
|
19
|
+
*/
|
|
20
|
+
function hasWebhooksDeclared(descriptionValue: TSESTree.ObjectExpression): boolean {
|
|
21
|
+
const webhooksProperty = findObjectProperty(descriptionValue, 'webhooks');
|
|
22
|
+
if (webhooksProperty?.value.type !== AST_NODE_TYPES.ArrayExpression) return false;
|
|
23
|
+
return webhooksProperty.value.elements.length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Returns true when the property defines a (possibly async) method named `name`. */
|
|
27
|
+
function isMethodProperty(property: TSESTree.ObjectLiteralElement, name: string): boolean {
|
|
28
|
+
if (property.type !== AST_NODE_TYPES.Property) return false;
|
|
29
|
+
if (property.computed) return false;
|
|
30
|
+
|
|
31
|
+
const keyMatches =
|
|
32
|
+
(property.key.type === AST_NODE_TYPES.Identifier && property.key.name === name) ||
|
|
33
|
+
(property.key.type === AST_NODE_TYPES.Literal && property.key.value === name);
|
|
34
|
+
if (!keyMatches) return false;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
property.value.type === AST_NODE_TYPES.FunctionExpression ||
|
|
38
|
+
property.value.type === AST_NODE_TYPES.ArrowFunctionExpression
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findMissingMethods(group: TSESTree.ObjectExpression): RequiredMethod[] {
|
|
43
|
+
return REQUIRED_METHODS.filter(
|
|
44
|
+
(method) => !group.properties.some((property) => isMethodProperty(property, method)),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const WebhookLifecycleCompleteRule = createRule({
|
|
49
|
+
name: 'webhook-lifecycle-complete',
|
|
50
|
+
meta: {
|
|
51
|
+
type: 'problem',
|
|
52
|
+
docs: {
|
|
53
|
+
description:
|
|
54
|
+
'Require webhook trigger nodes to implement the complete webhookMethods lifecycle (checkExists, create, delete)',
|
|
55
|
+
},
|
|
56
|
+
messages: {
|
|
57
|
+
missingWebhookMethods:
|
|
58
|
+
'Webhook trigger node is missing the `webhookMethods` property. Implement `checkExists`, `create`, and `delete` to register, verify, and clean up the webhook on the third-party service.',
|
|
59
|
+
missingLifecycleMethod:
|
|
60
|
+
'Webhook trigger lifecycle is incomplete. `webhookMethods.{{group}}` is missing: {{missing}}. All of `checkExists`, `create`, and `delete` must be implemented.',
|
|
61
|
+
},
|
|
62
|
+
schema: [],
|
|
63
|
+
},
|
|
64
|
+
defaultOptions: [],
|
|
65
|
+
create(context) {
|
|
66
|
+
return {
|
|
67
|
+
ClassDeclaration(node) {
|
|
68
|
+
if (!isNodeTypeClass(node)) return;
|
|
69
|
+
|
|
70
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
71
|
+
if (!descriptionProperty) return;
|
|
72
|
+
|
|
73
|
+
const descriptionValue = descriptionProperty.value;
|
|
74
|
+
if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
75
|
+
|
|
76
|
+
const webhookMethodsProperty = findClassProperty(node, 'webhookMethods');
|
|
77
|
+
|
|
78
|
+
if (!hasWebhooksDeclared(descriptionValue) && !webhookMethodsProperty) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!webhookMethodsProperty?.value) {
|
|
83
|
+
context.report({
|
|
84
|
+
node: node.id ?? node,
|
|
85
|
+
messageId: 'missingWebhookMethods',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (webhookMethodsProperty.value.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const groupProperty of webhookMethodsProperty.value.properties) {
|
|
95
|
+
if (groupProperty.type !== AST_NODE_TYPES.Property) continue;
|
|
96
|
+
if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression) continue;
|
|
97
|
+
|
|
98
|
+
const groupName =
|
|
99
|
+
groupProperty.key.type === AST_NODE_TYPES.Identifier
|
|
100
|
+
? groupProperty.key.name
|
|
101
|
+
: groupProperty.key.type === AST_NODE_TYPES.Literal
|
|
102
|
+
? String(groupProperty.key.value)
|
|
103
|
+
: 'default';
|
|
104
|
+
|
|
105
|
+
const missing = findMissingMethods(groupProperty.value);
|
|
106
|
+
if (missing.length === 0) continue;
|
|
107
|
+
|
|
108
|
+
context.report({
|
|
109
|
+
node: groupProperty.key,
|
|
110
|
+
messageId: 'missingLifecycleMethod',
|
|
111
|
+
data: {
|
|
112
|
+
group: groupName,
|
|
113
|
+
missing: missing.map((m) => `\`${m}\``).join(', '),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
});
|