Skip to content

Commit 3f804b3

Browse files
authored
fix: package review fixes (#20)
* fix: dep and item pairing fix * fix: coding standard and continueOnFail * fix: n8n-core peer dep for tests * chore: remove old testing file * feat: mocked tests without n8n-core * fix: formatting fixes * fix: remove n8n-core
1 parent 4fa625f commit 3f804b3

File tree

8 files changed

+184
-1650
lines changed

8 files changed

+184
-1650
lines changed

nodes/ApifyContentCrawler/ApifyContentCrawler.node.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,33 @@ export class ApifyContentCrawler implements INodeType {
6060

6161
methods = methods;
6262

63-
async execute(this: IExecuteFunctions) {
63+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
6464
const items = this.getInputData();
6565
const returnData: INodeExecutionData[] = [];
6666

6767
for (let i = 0; i < items.length; i++) {
68-
const data = await actorsRouter.call(this, i);
69-
// `data` may be an array of items or a single item, so we either push the spreaded array or the single item
70-
if (Array.isArray(data)) {
71-
returnData.push(...data);
72-
} else {
73-
returnData.push(data);
68+
try {
69+
const data = await actorsRouter.call(this, i);
70+
71+
const addPairedItem = (item: INodeExecutionData) => ({
72+
...item,
73+
pairedItem: { item: i },
74+
});
75+
76+
if (Array.isArray(data)) {
77+
returnData.push(...data.map(addPairedItem));
78+
} else {
79+
returnData.push(addPairedItem(data));
80+
}
81+
} catch (error) {
82+
if (this.continueOnFail()) {
83+
returnData.push({
84+
json: { error: error.message },
85+
pairedItem: { item: i },
86+
});
87+
continue;
88+
}
89+
throw error;
7490
}
7591
}
7692

nodes/ApifyContentCrawler/__tests__/Apify.node.spec.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { ACTOR_ID, ApifyContentCrawler } from '../ApifyContentCrawler.node';
1+
import { ApifyContentCrawler } from '../ApifyContentCrawler.node';
22
import { executeWorkflow } from './utils/executeWorkflow';
33
import { CredentialsHelper } from './utils/credentialHelper';
44
import { getRunTaskDataByNodeName, getTaskData } from './utils/getNodeResultData';
5-
import nock from 'nock';
65
import * as fixtures from './utils/fixtures';
76

87
describe('Apify Node', () => {
@@ -36,9 +35,6 @@ describe('Apify Node', () => {
3635

3736
describe('actors', () => {
3837
describe('run-actor', () => {
39-
const mockRunActor = fixtures.runActorResult();
40-
const mockBuild = fixtures.getBuildResult();
41-
const mockFinishedRun = fixtures.getSuccessRunResult();
4238
const mockResultDataset = fixtures.getDatasetItems();
4339

4440
const tests = [
@@ -52,17 +48,6 @@ describe('Apify Node', () => {
5248
test.each(tests)(
5349
'$name should run the WCC actor correctly',
5450
async ({ workflowJsonName, nodeName }) => {
55-
const scope = nock('https://api.apify.com')
56-
.get(`/v2/acts/${ACTOR_ID}/builds/default`)
57-
.reply(200, mockBuild)
58-
.post(`/v2/acts/${ACTOR_ID}/runs`)
59-
.query({ waitForFinish: 0 })
60-
.reply(200, mockRunActor)
61-
.get('/v2/actor-runs/5rsC83CHinQwPlsSI')
62-
.reply(200, mockFinishedRun)
63-
.get('/v2/datasets/63kMAihbWVgBvEAZ2/items')
64-
.reply(200, mockResultDataset);
65-
6651
const workflow = require(`./workflows/actors/${workflowJsonName}`);
6752
const { executionData } = await executeWorkflow({
6853
credentialsHelper,
@@ -79,9 +64,6 @@ describe('Apify Node', () => {
7964
expect(typeof data).toBe('object');
8065

8166
expect(data).toEqual(mockResultDataset[0]);
82-
83-
console.log(`Pending mocks for ${workflowJsonName}:`, scope.pendingMocks());
84-
expect(scope.isDone()).toBe(true);
8567
},
8668
);
8769
});
Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,33 @@
1-
import { Credentials } from 'n8n-core';
1+
// utils/credentialHelper.ts
22
import {
33
ICredentialDataDecryptedObject,
44
ICredentialsHelper,
5-
IHttpRequestHelper,
5+
INodeProperties,
66
IHttpRequestOptions,
7+
IHttpRequestHelper,
8+
IRequestOptionsSimplified,
79
INode,
8-
INodeCredentialsDetails,
9-
ICredentials,
10-
INodeProperties,
11-
IWorkflowExecuteAdditionalData,
12-
WorkflowExecuteMode,
13-
IExecuteData,
14-
ICredentialsExpressionResolveValues,
1510
Workflow,
16-
IRequestOptionsSimplified,
1711
} from 'n8n-workflow';
1812

19-
export class CredentialsHelper extends ICredentialsHelper {
20-
private credentials: Record<string, ICredentialDataDecryptedObject>;
13+
export class CredentialsHelper implements ICredentialsHelper {
14+
constructor(private credentials: Record<string, ICredentialDataDecryptedObject>) {}
2115

22-
constructor(credentials: Record<string, ICredentialDataDecryptedObject>) {
23-
super();
24-
this.credentials = credentials;
16+
getParentTypes(name: string): string[] {
17+
return [];
2518
}
2619

27-
getParentTypes(name: string): string[] {
20+
async getDecrypted(): Promise<ICredentialDataDecryptedObject> {
21+
return this.credentials['apifyApi'];
22+
}
23+
24+
async getCredentials(): Promise<any> {
25+
return {};
26+
}
27+
28+
async updateCredentials(): Promise<void> {}
29+
30+
getCredentialsProperties(type: string): INodeProperties[] {
2831
return [];
2932
}
3033

@@ -35,6 +38,7 @@ export class CredentialsHelper extends ICredentialsHelper {
3538
workflow: Workflow,
3639
node: INode,
3740
): Promise<IHttpRequestOptions> {
41+
// For testing, just return requestOptions as-is
3842
return requestOptions as IHttpRequestOptions;
3943
}
4044

@@ -47,33 +51,4 @@ export class CredentialsHelper extends ICredentialsHelper {
4751
): Promise<ICredentialDataDecryptedObject | undefined> {
4852
return undefined;
4953
}
50-
51-
async getCredentials(
52-
nodeCredentials: INodeCredentialsDetails,
53-
type: string,
54-
): Promise<ICredentials> {
55-
return new Credentials({ id: null, name: '' }, '', '');
56-
}
57-
58-
async getDecrypted(
59-
additionalData: IWorkflowExecuteAdditionalData,
60-
nodeCredentials: INodeCredentialsDetails,
61-
type: string,
62-
mode: WorkflowExecuteMode,
63-
executeData?: IExecuteData,
64-
raw?: boolean,
65-
expressionResolveValues?: ICredentialsExpressionResolveValues,
66-
): Promise<ICredentialDataDecryptedObject> {
67-
return this.credentials[type];
68-
}
69-
70-
async updateCredentials(
71-
nodeCredentials: INodeCredentialsDetails,
72-
type: string,
73-
data: ICredentialDataDecryptedObject,
74-
): Promise<void> {}
75-
76-
getCredentialsProperties(type: string): INodeProperties[] {
77-
return [];
78-
}
7954
}
Lines changed: 96 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,115 @@
11
import {
2-
createDeferredPromise,
32
ICredentialsHelper,
4-
IExecuteWorkflowInfo,
3+
IExecuteFunctions,
4+
IExecuteSingleFunctions,
5+
INodeExecutionData,
56
IRun,
6-
IWorkflowBase,
7-
IWorkflowExecuteAdditionalData,
8-
LoggerProxy,
9-
Workflow,
7+
ITaskData,
108
} from 'n8n-workflow';
11-
import { WorkflowExecute, ExecutionLifecycleHooks } from 'n8n-core';
129
import { nodeTypes } from './nodeTypesClass';
10+
import * as fixtures from './fixtures';
1311

1412
export type ExecuteWorkflowArgs = {
1513
workflow: any;
1614
credentialsHelper: ICredentialsHelper;
1715
};
1816

19-
export const executeWorkflow = async ({ credentialsHelper, ...args }: ExecuteWorkflowArgs) => {
20-
LoggerProxy.init({
21-
debug() {},
22-
error() {},
23-
info() {},
24-
warn() {},
25-
});
17+
export const executeWorkflow = async ({
18+
credentialsHelper,
19+
workflow: workflowJson,
20+
}: ExecuteWorkflowArgs): Promise<{ executionData: IRun }> => {
21+
// Pick the first node (your crawler)
22+
const [node] = workflowJson.nodes;
2623

27-
const workflow = new Workflow({
28-
id: 'test',
29-
active: true,
30-
connections: args.workflow.connections,
31-
nodes: args.workflow.nodes,
32-
nodeTypes,
33-
});
24+
// Minimal fake executeFunctions
25+
const fakeExecuteFunctions = {
26+
getCredentials: async (name: string) => {
27+
return credentialsHelper.getDecrypted({} as any, { name } as any, name, 'manual');
28+
},
29+
getNodeParameter: (parameterName: string) => {
30+
return node.parameters[parameterName];
31+
},
32+
getInputData: (): INodeExecutionData[] => {
33+
// Provide at least one empty item so loops like `for (let i = 0; i < items.length; i++)`
34+
// still work
35+
return [{ json: {} }];
36+
},
37+
continueOnFail: () => {
38+
return false;
39+
},
40+
getNode: () => {
41+
return node;
42+
},
43+
helpers: {
44+
requestWithAuthentication: async function (
45+
this: IExecuteFunctions,
46+
_credentialType: string,
47+
options: any,
48+
) {
49+
const url = options.url as string;
3450

35-
const waitPromise = createDeferredPromise<IRun>();
51+
if (url.includes('/builds/default')) {
52+
return fixtures.getBuildResult();
53+
}
54+
if (url.includes('/runs')) {
55+
return fixtures.runActorResult();
56+
}
57+
if (url.includes('/actor-runs/')) {
58+
return fixtures.getSuccessRunResult();
59+
}
60+
if (url.includes('/actors/')) {
61+
return fixtures.getActorResult();
62+
}
63+
if (url.includes('/datasets/')) {
64+
return fixtures.getDatasetItems();
65+
}
3666

37-
const workflowData: IWorkflowBase = {
38-
id: 'test',
39-
name: 'test',
40-
createdAt: new Date(),
41-
updatedAt: new Date(),
42-
active: true,
43-
nodes: args.workflow.nodes,
44-
connections: args.workflow.connections,
45-
};
46-
47-
const additionalData: IWorkflowExecuteAdditionalData = {
48-
credentialsHelper,
49-
hooks: new ExecutionLifecycleHooks('trigger', '1', workflowData),
50-
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
51-
restApiUrl: 'http://localhost:5678',
52-
webhookBaseUrl: 'http://localhost:5678',
53-
webhookWaitingBaseUrl: 'http://localhost:5678',
54-
webhookTestBaseUrl: 'http://localhost:5678',
55-
userId: 'userId',
56-
instanceBaseUrl: 'http://localhost:5678',
57-
formWaitingBaseUrl: 'http://localhost:5678',
58-
variables: {},
59-
secretsHelpers: {} as any,
60-
logAiEvent: async () => {},
61-
startRunnerTask: (async () => {}) as any,
62-
};
67+
throw new Error(`Unhandled request in fixture stub: ${url}`);
68+
},
69+
returnJsonArray: (items: any[]) => {
70+
return items.map((i) => ({ json: i }));
71+
},
72+
constructExecutionMetaData: (inputData: any, _options: any) => {
73+
return inputData;
74+
},
75+
},
76+
} as unknown as IExecuteFunctions & IExecuteSingleFunctions;
6377

64-
const workflowExecute = new WorkflowExecute(additionalData, 'cli');
78+
// Run the node directly
79+
const nodeType = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
80+
if (!('execute' in nodeType) || typeof nodeType.execute !== 'function') {
81+
throw new Error(`Node ${node.type} has no execute() method`);
82+
}
83+
const result = await (nodeType.execute as Function).call(fakeExecuteFunctions);
6584

66-
const executionData = await workflowExecute.run(workflow);
85+
// Build fake ITaskData
86+
const taskData: ITaskData = {
87+
startTime: Date.now(),
88+
executionTime: 1,
89+
executionStatus: 'success',
90+
data: { main: result as any },
91+
source: [
92+
{
93+
previousNode: '',
94+
},
95+
],
96+
};
6797

68-
return {
69-
workflow,
70-
waitPromise,
71-
executionData,
72-
additionalData,
98+
// Wrap in fake IRun-like structure
99+
const executionData: IRun = {
100+
mode: 'manual',
101+
status: 'success',
102+
data: {
103+
resultData: {
104+
runData: {
105+
[node.name]: [taskData],
106+
},
107+
},
108+
},
109+
finished: true,
110+
startedAt: new Date(),
111+
stoppedAt: new Date(),
73112
};
113+
114+
return { executionData };
74115
};

nodes/ApifyContentCrawler/__tests__/workflows/actors/run-actor-standard.workflow.json

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)