Skip to content

Commit

Permalink
Merge branch 'master' of github.com:n8n-io/n8n into linear-add-identi…
Browse files Browse the repository at this point in the history
…fier
  • Loading branch information
Joffcom committed May 22, 2024
2 parents c576aca + 9da9368 commit 97afc50
Show file tree
Hide file tree
Showing 113 changed files with 2,877 additions and 1,688 deletions.
6 changes: 5 additions & 1 deletion cypress/e2e/5-ndv.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,11 @@ describe('NDV', () => {
});

it('should not retrieve remote options when a parameter value changes', () => {
cy.intercept('/rest/dynamic-node-parameters/options?**', cy.spy().as('fetchParameterOptions'));
cy.intercept(
'POST',
'/rest/dynamic-node-parameters/options',
cy.spy().as('fetchParameterOptions'),
);
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
// Type something into the field
ndv.actions.typeIntoParameterInput('otherField', 'test');
Expand Down
6 changes: 3 additions & 3 deletions cypress/fixtures/Multiple_trigger_node_rerun.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
{
"parameters": {
"url": "https://random-data-api.com/api/v2/users?size=5",
"url": "https://internal.users.n8n.cloud/webhook/random-data-api",
"options": {}
},
"id": "22511d75-ab54-49e1-b8af-08b8b3372373",
Expand All @@ -28,7 +28,7 @@
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.first_name,\n firstnNameReversed: item.json.first_name_BUG.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();"
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.firstname,\n firstnNameReversed: item.json.firstname.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();"
},
"id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21",
"name": "do something with them",
Expand Down Expand Up @@ -130,4 +130,4 @@
},
"id": "PymcwIrbqgNh3O0K",
"tags": []
}
}
85 changes: 34 additions & 51 deletions packages/cli/src/controllers/dynamicNodeParameters.controller.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,27 @@
import type { RequestHandler } from 'express';
import { NextFunction, Response } from 'express';
import type {
INodeListSearchResult,
INodePropertyOptions,
ResourceMapperFields,
} from 'n8n-workflow';
import type { INodePropertyOptions } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';

import { Get, Middleware, RestController } from '@/decorators';
import { Post, RestController } from '@/decorators';
import { getBase } from '@/WorkflowExecuteAdditionalData';
import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service';
import { DynamicNodeParametersRequest } from '@/requests';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';

const assertMethodName: RequestHandler = (req, res, next) => {
const { methodName } = req.query as DynamicNodeParametersRequest.BaseRequest['query'];
if (!methodName) {
throw new BadRequestError('Parameter methodName is required.');
}
next();
};

@RestController('/dynamic-node-parameters')
export class DynamicNodeParametersController {
constructor(private readonly service: DynamicNodeParametersService) {}

@Middleware()
parseQueryParams(req: DynamicNodeParametersRequest.BaseRequest, _: Response, next: NextFunction) {
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.query;
if (!nodeTypeAndVersion) {
throw new BadRequestError('Parameter nodeTypeAndVersion is required.');
}
if (!currentNodeParameters) {
throw new BadRequestError('Parameter currentNodeParameters is required.');
}

req.params = {
nodeTypeAndVersion: jsonParse(nodeTypeAndVersion),
currentNodeParameters: jsonParse(currentNodeParameters),
credentials: credentials ? jsonParse(credentials) : undefined,
};

next();
}

/** Returns parameter values which normally get loaded from an external API or get generated dynamically */
@Get('/options')
@Post('/options')
async getOptions(req: DynamicNodeParametersRequest.Options): Promise<INodePropertyOptions[]> {
const { path, methodName, loadOptions } = req.query;
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.params;
const {
credentials,
currentNodeParameters,
nodeTypeAndVersion,
path,
methodName,
loadOptions,
} = req.body;

const additionalData = await getBase(req.user.id, currentNodeParameters);

if (methodName) {
Expand All @@ -75,13 +48,22 @@ export class DynamicNodeParametersController {
return [];
}

@Get('/resource-locator-results', { middlewares: [assertMethodName] })
async getResourceLocatorResults(
req: DynamicNodeParametersRequest.ResourceLocatorResults,
): Promise<INodeListSearchResult | undefined> {
const { path, methodName, filter, paginationToken } = req.query;
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.params;
@Post('/resource-locator-results')
async getResourceLocatorResults(req: DynamicNodeParametersRequest.ResourceLocatorResults) {
const {
path,
methodName,
filter,
paginationToken,
credentials,
currentNodeParameters,
nodeTypeAndVersion,
} = req.body;

if (!methodName) throw new BadRequestError('Missing `methodName` in request body');

const additionalData = await getBase(req.user.id, currentNodeParameters);

return await this.service.getResourceLocatorResults(
methodName,
path,
Expand All @@ -94,13 +76,14 @@ export class DynamicNodeParametersController {
);
}

@Get('/resource-mapper-fields', { middlewares: [assertMethodName] })
async getResourceMappingFields(
req: DynamicNodeParametersRequest.ResourceMapperFields,
): Promise<ResourceMapperFields | undefined> {
const { path, methodName } = req.query;
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.params;
@Post('/resource-mapper-fields')
async getResourceMappingFields(req: DynamicNodeParametersRequest.ResourceMapperFields) {
const { path, methodName, credentials, currentNodeParameters, nodeTypeAndVersion } = req.body;

if (!methodName) throw new BadRequestError('Missing `methodName` in request body');

const additionalData = await getBase(req.user.id, currentNodeParameters);

return await this.service.getResourceMappingFields(
methodName,
path,
Expand Down
16 changes: 6 additions & 10 deletions packages/cli/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,21 +393,17 @@ export declare namespace OAuthRequest {
// /dynamic-node-parameters
// ----------------------------------
export declare namespace DynamicNodeParametersRequest {
type BaseRequest<QueryParams = {}> = AuthenticatedRequest<
{
nodeTypeAndVersion: INodeTypeNameVersion;
currentNodeParameters: INodeParameters;
credentials?: INodeCredentials;
},
type BaseRequest<RequestBody = {}> = AuthenticatedRequest<
{},
{},
{
path: string;
nodeTypeAndVersion: string;
currentNodeParameters: string;
nodeTypeAndVersion: INodeTypeNameVersion;
currentNodeParameters: INodeParameters;
methodName?: string;
credentials?: string;
} & QueryParams
credentials?: INodeCredentials;
} & RequestBody,
{}
>;

/** GET /dynamic-node-parameters/options */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { SuperTest, Test } from 'supertest';
import { createOwner } from '../shared/db/users';
import { setupTestServer } from '../shared/utils';
import * as AdditionalData from '@/WorkflowExecuteAdditionalData';
import type {
INodeListSearchResult,
IWorkflowExecuteAdditionalData,
ResourceMapperFields,
} from 'n8n-workflow';
import { mock } from 'jest-mock-extended';
import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service';

describe('DynamicNodeParametersController', () => {
const testServer = setupTestServer({ endpointGroups: ['dynamic-node-parameters'] });
let ownerAgent: SuperTest<Test>;

beforeAll(async () => {
const owner = await createOwner();
ownerAgent = testServer.authAgentFor(owner);
});

const commonRequestParams = {
credentials: {},
currentNodeParameters: {},
nodeTypeAndVersion: {},
path: 'path',
methodName: 'methodName',
};

describe('POST /dynamic-node-parameters/options', () => {
jest.spyOn(AdditionalData, 'getBase').mockResolvedValue(mock<IWorkflowExecuteAdditionalData>());

it('should take params via body', async () => {
jest
.spyOn(DynamicNodeParametersService.prototype, 'getOptionsViaMethodName')
.mockResolvedValue([]);

await ownerAgent
.post('/dynamic-node-parameters/options')
.send({
...commonRequestParams,
loadOptions: 'loadOptions',
})
.expect(200);
});
});

describe('POST /dynamic-node-parameters/resource-locator-results', () => {
it('should take params via body', async () => {
jest
.spyOn(DynamicNodeParametersService.prototype, 'getResourceLocatorResults')
.mockResolvedValue(mock<INodeListSearchResult>());

await ownerAgent
.post('/dynamic-node-parameters/resource-locator-results')
.send({
...commonRequestParams,
filter: 'filter',
paginationToken: 'paginationToken',
})
.expect(200);
});
});

describe('POST /dynamic-node-parameters/resource-mapper-fields', () => {
it('should take params via body', async () => {
jest
.spyOn(DynamicNodeParametersService.prototype, 'getResourceMappingFields')
.mockResolvedValue(mock<ResourceMapperFields>());

await ownerAgent
.post('/dynamic-node-parameters/resource-mapper-fields')
.send({
...commonRequestParams,
loadOptions: 'loadOptions',
})
.expect(200);
});
});
});
3 changes: 2 additions & 1 deletion packages/cli/test/integration/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ type EndpointGroup =
| 'invitations'
| 'debug'
| 'project'
| 'role';
| 'role'
| 'dynamic-node-parameters';

export interface SetupProps {
endpointGroups?: EndpointGroup[];
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/test/integration/shared/utils/testServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ export const setupTestServer = ({
const { RoleController } = await import('@/controllers/role.controller');
registerController(app, RoleController);
break;

case 'dynamic-node-parameters':
const { DynamicNodeParametersController } = await import(
'@/controllers/dynamicNodeParameters.controller'
);
registerController(app, DynamicNodeParametersController);
break;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export namespace n8n {
}
}

export type ExtendedValidationResult = Partial<ValidationResult> & { fieldName?: string };
export type ExtendedValidationResult = ValidationResult & { fieldName?: string };
48 changes: 25 additions & 23 deletions packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import type {
WorkflowActivateMode,
WorkflowExecuteMode,
CallbackManager,
INodeParameters,
} from 'n8n-workflow';
import {
ExpressionError,
Expand Down Expand Up @@ -2121,13 +2122,12 @@ export function cleanupParameterData(inputData: NodeParameterValueType): void {
}

if (typeof inputData === 'object') {
type Key = keyof typeof inputData;
(Object.keys(inputData) as Key[]).forEach((key) => {
const value = inputData[key];
Object.keys(inputData).forEach((key) => {
const value = (inputData as INodeParameters)[key];
if (typeof value === 'object') {
if (DateTime.isDateTime(value)) {
// Is a special luxon date so convert to string
inputData[key] = value.toString();
(inputData as INodeParameters)[key] = value.toString();
} else {
cleanupParameterData(value);
}
Expand Down Expand Up @@ -2230,28 +2230,30 @@ const validateCollection = (
return validationResult;
}

for (const value of Array.isArray(validationResult.newValue)
? (validationResult.newValue as IDataObject[])
: [validationResult.newValue as IDataObject]) {
for (const key of Object.keys(value)) {
if (!validationMap[key]) continue;
if (validationResult.valid) {
for (const value of Array.isArray(validationResult.newValue)
? (validationResult.newValue as IDataObject[])
: [validationResult.newValue as IDataObject]) {
for (const key of Object.keys(value)) {
if (!validationMap[key]) continue;

const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, {
valueOptions: validationMap[key].options,
});
const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, {
valueOptions: validationMap[key].options,
});

if (!fieldValidationResult.valid) {
throw new ExpressionError(
`Invalid input for field '${validationMap[key].displayName}' inside '${propertyDescription.displayName}' in [item ${itemIndex}]`,
{
description: fieldValidationResult.errorMessage,
runIndex,
itemIndex,
nodeCause: node.name,
},
);
if (!fieldValidationResult.valid) {
throw new ExpressionError(
`Invalid input for field '${validationMap[key].displayName}' inside '${propertyDescription.displayName}' in [item ${itemIndex}]`,
{
description: fieldValidationResult.errorMessage,
runIndex,
itemIndex,
nodeCause: node.name,
},
);
}
value[key] = fieldValidationResult.newValue;
}
value[key] = fieldValidationResult.newValue;
}
}

Expand Down
Loading

0 comments on commit 97afc50

Please sign in to comment.