mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(Jira Software Node): Migrate from soon deprecated endpoints to get issues (#14821)
This commit is contained in:
@@ -75,6 +75,33 @@ export async function jiraSoftwareCloudApiRequest(
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePagination(
|
||||
body: any,
|
||||
query: IDataObject,
|
||||
paginationType: 'offset' | 'token',
|
||||
responseData?: any,
|
||||
): boolean {
|
||||
if (!responseData) {
|
||||
if (paginationType === 'offset') {
|
||||
query.startAt = 0;
|
||||
query.maxResults = 100;
|
||||
} else {
|
||||
body.maxResults = 100;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (paginationType === 'offset') {
|
||||
const nextStartAt = (responseData.startAt as number) + (responseData.maxResults as number);
|
||||
query.startAt = nextStartAt;
|
||||
return nextStartAt < responseData.total;
|
||||
} else {
|
||||
body.nextPageToken = responseData.nextPageToken as string;
|
||||
return !!responseData.nextPageToken;
|
||||
}
|
||||
}
|
||||
|
||||
export async function jiraSoftwareCloudApiRequestAllItems(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
propertyName: string,
|
||||
@@ -82,29 +109,17 @@ export async function jiraSoftwareCloudApiRequestAllItems(
|
||||
method: IHttpRequestMethods,
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
paginationType: 'offset' | 'token' = 'offset',
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
|
||||
query.startAt = 0;
|
||||
query.maxResults = 100;
|
||||
if (method !== 'GET') {
|
||||
body.startAt = 0;
|
||||
body.maxResults = 100;
|
||||
}
|
||||
|
||||
let hasNextPage = handlePagination(body, query, paginationType);
|
||||
do {
|
||||
responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query);
|
||||
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
|
||||
query.startAt = (responseData.startAt as number) + (responseData.maxResults as number);
|
||||
if (method !== 'GET') {
|
||||
body.startAt = (responseData.startAt as number) + (responseData.maxResults as number);
|
||||
}
|
||||
} while (
|
||||
(responseData.startAt as number) + (responseData.maxResults as number) <
|
||||
responseData.total
|
||||
);
|
||||
hasNextPage = handlePagination(body, query, paginationType, responseData);
|
||||
} while (hasNextPage);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
@@ -804,12 +804,16 @@ export class Jira implements INodeType {
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const options = this.getNodeParameter('options', i);
|
||||
const body: IDataObject = {};
|
||||
if (options.fields) {
|
||||
body.fields = (options.fields as string).split(',');
|
||||
if (!options.fields) {
|
||||
// By default, the new endpoint returns only the ids, before it used to return `*navigable` fields
|
||||
options.fields = '*navigable';
|
||||
}
|
||||
if (options.jql) {
|
||||
body.jql = options.jql as string;
|
||||
body.fields = (options.fields as string).split(',');
|
||||
if (!options.jql) {
|
||||
// Jira API returns an error if the JQL query is unbounded (i.e. does not include any filters)
|
||||
options.jql = 'created >= "1970-01-01"';
|
||||
}
|
||||
body.jql = options.jql as string;
|
||||
if (options.expand) {
|
||||
if (typeof options.expand === 'string') {
|
||||
body.expand = options.expand.split(',');
|
||||
@@ -821,16 +825,18 @@ export class Jira implements INodeType {
|
||||
responseData = await jiraSoftwareCloudApiRequestAllItems.call(
|
||||
this,
|
||||
'issues',
|
||||
'/api/2/search',
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
body,
|
||||
{},
|
||||
'token',
|
||||
);
|
||||
} else {
|
||||
const limit = this.getNodeParameter('limit', i);
|
||||
body.maxResults = limit;
|
||||
responseData = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
'/api/2/search',
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
body,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type DeepMockProxy, mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { jiraSoftwareCloudApiRequestAllItems } from '../GenericFunctions';
|
||||
import { handlePagination, jiraSoftwareCloudApiRequestAllItems } from '../GenericFunctions';
|
||||
|
||||
describe('Jira -> GenericFunctions', () => {
|
||||
describe('jiraSoftwareCloudApiRequestAllItems', () => {
|
||||
@@ -54,4 +54,89 @@ describe('Jira -> GenericFunctions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePagination', () => {
|
||||
it('should initialize offset pagination parameters when responseData is not provided', () => {
|
||||
const body = {};
|
||||
const query: IDataObject = {};
|
||||
|
||||
const result = handlePagination(body, query, 'offset');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query.startAt).toBe(0);
|
||||
expect(query.maxResults).toBe(100);
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should initialize token pagination parameters when responseData is not provided', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
|
||||
const result = handlePagination(body, query, 'token');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query).toEqual({});
|
||||
expect(body.maxResults).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle offset pagination with more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
startAt: 0,
|
||||
maxResults: 100,
|
||||
total: 250,
|
||||
};
|
||||
|
||||
const result = handlePagination(body, query, 'offset', responseData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(query.startAt).toBe(100);
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle offset pagination with no more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
startAt: 200,
|
||||
maxResults: 100,
|
||||
total: 250,
|
||||
};
|
||||
|
||||
const result = handlePagination(body, query, 'offset', responseData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(query.startAt).toBe(300);
|
||||
expect(body).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle token pagination with more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
nextPageToken: 'someToken123',
|
||||
};
|
||||
|
||||
const result = handlePagination(body, query, 'token', responseData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(body.nextPageToken).toBe('someToken123');
|
||||
expect(query).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle token pagination with no more pages available', () => {
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
const responseData = {
|
||||
nextPageToken: '',
|
||||
};
|
||||
|
||||
const result = handlePagination(body, query, 'token', responseData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(body.nextPageToken).toBe('');
|
||||
expect(query).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
152
packages/nodes-base/nodes/Jira/test/node/Jira.node.test.ts
Normal file
152
packages/nodes-base/nodes/Jira/test/node/Jira.node.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { DeepMockProxy } from 'jest-mock-extended';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import * as GenericFunctions from '../../GenericFunctions';
|
||||
import { Jira } from '../../Jira.node';
|
||||
|
||||
jest.mock('../../GenericFunctions', () => ({
|
||||
jiraSoftwareCloudApiRequest: jest.fn().mockResolvedValue({ issues: [] }),
|
||||
}));
|
||||
|
||||
const jiraSoftwareCloudApiRequestMock = GenericFunctions.jiraSoftwareCloudApiRequest as jest.Mock;
|
||||
|
||||
describe('Jira Node', () => {
|
||||
let jiraNode: Jira;
|
||||
let executeFunctionsMock: DeepMockProxy<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jiraNode = new Jira();
|
||||
executeFunctionsMock = mockDeep<IExecuteFunctions>();
|
||||
executeFunctionsMock.getInputData.mockReturnValue([{ json: {} }]);
|
||||
executeFunctionsMock.helpers.returnJsonArray.mockReturnValue([]);
|
||||
executeFunctionsMock.helpers.constructExecutionMetaData.mockReturnValue([]);
|
||||
});
|
||||
|
||||
describe('issue getAll', () => {
|
||||
it('should set default fields to "*navigable" when not provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { fields: undefined };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
fields: ['*navigable'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set default JQL filter to "created >= 1970-01-01" when not provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { jql: undefined };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
jql: 'created >= "1970-01-01"',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom fields when provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { fields: 'summary,description' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
fields: ['summary', 'description'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom JQL filter when provided', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
switch (parameterName) {
|
||||
case 'resource':
|
||||
return 'issue';
|
||||
case 'operation':
|
||||
return 'getAll';
|
||||
case 'jiraVersion':
|
||||
return 'cloud';
|
||||
case 'returnAll':
|
||||
return false;
|
||||
case 'limit':
|
||||
return 10;
|
||||
case 'options':
|
||||
return { jql: 'project = TEST' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await jiraNode.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(jiraSoftwareCloudApiRequestMock).toHaveBeenCalledWith(
|
||||
'/api/2/search/jql',
|
||||
'POST',
|
||||
expect.objectContaining({
|
||||
jql: 'project = TEST',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user