mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
feat(Okta Node): Add Okta Node (#10278)
Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
71b6c67179
commit
5cac0f339d
|
@ -33,8 +33,11 @@ function findReferencedMethods(obj, refs = {}, latestName = '') {
|
|||
const knownCredentials = loader.known.credentials;
|
||||
const credentialTypes = Object.values(loader.credentialTypes).map((data) => {
|
||||
const credentialType = data.type;
|
||||
if (knownCredentials[credentialType.name].supportedNodes?.length > 0) {
|
||||
delete credentialType.httpRequestNode;
|
||||
if (
|
||||
knownCredentials[credentialType.name].supportedNodes?.length > 0 &&
|
||||
credentialType.httpRequestNode
|
||||
) {
|
||||
credentialType.httpRequestNode.hidden = true;
|
||||
}
|
||||
return credentialType;
|
||||
});
|
||||
|
|
|
@ -206,7 +206,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
|||
});
|
||||
|
||||
const httpOnlyCredentialTypes = computed(() => {
|
||||
return allCredentialTypes.value.filter((credentialType) => credentialType.httpRequestNode);
|
||||
return allCredentialTypes.value.filter(
|
||||
(credentialType) => credentialType.httpRequestNode && !credentialType.httpRequestNode.hidden,
|
||||
);
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
|
|
@ -30,12 +30,13 @@ export class OktaApi implements ICredentialType {
|
|||
placeholder: 'https://dev-123456.okta.com',
|
||||
},
|
||||
{
|
||||
displayName: 'SSWS Access Token',
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string',
|
||||
typeOptions: { password: true },
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Secure Session Web Service Access Token',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
3
packages/nodes-base/nodes/Okta/Okta.dark.svg
Normal file
3
packages/nodes-base/nodes/Okta/Okta.dark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.9 KiB |
56
packages/nodes-base/nodes/Okta/Okta.node.ts
Normal file
56
packages/nodes-base/nodes/Okta/Okta.node.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { userFields, userOperations } from './UserDescription';
|
||||
import { getUsers } from './UserFunctions';
|
||||
|
||||
export class Okta implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Okta',
|
||||
name: 'okta',
|
||||
icon: { light: 'file:Okta.svg', dark: 'file:Okta.dark.svg' },
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Use the Okta API',
|
||||
defaults: {
|
||||
name: 'Okta',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'oktaApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
returnFullResponse: true,
|
||||
baseURL: '={{$credentials.url.replace(new RegExp("/$"), "")}}',
|
||||
headers: {},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
|
||||
// USER
|
||||
...userOperations,
|
||||
...userFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch: {
|
||||
getUsers,
|
||||
},
|
||||
};
|
||||
}
|
3
packages/nodes-base/nodes/Okta/Okta.svg
Normal file
3
packages/nodes-base/nodes/Okta/Okta.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.9 KiB |
795
packages/nodes-base/nodes/Okta/UserDescription.ts
Normal file
795
packages/nodes-base/nodes/Okta/UserDescription.ts
Normal file
|
@ -0,0 +1,795 @@
|
|||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { getCursorPaginator, simplifyGetAllResponse, simplifyGetResponse } from './UserFunctions';
|
||||
const BASE_API_URL = '/api/v1/users/';
|
||||
export const userOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
// Create Operation
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new user',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: BASE_API_URL,
|
||||
qs: { activate: '={{$parameter["activate"]}}' },
|
||||
returnFullResponse: true,
|
||||
},
|
||||
},
|
||||
action: 'Create a new user',
|
||||
},
|
||||
// Delete Operation
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete an existing user',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '={{"/api/v1/users/" + $parameter["userId"]}}',
|
||||
returnFullResponse: true,
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "success": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete a user',
|
||||
},
|
||||
// Get Operation
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get details of a user',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '={{"/api/v1/users/" + $parameter["userId"]}}',
|
||||
returnFullResponse: true,
|
||||
qs: {},
|
||||
},
|
||||
output: {
|
||||
postReceive: [simplifyGetResponse],
|
||||
},
|
||||
},
|
||||
action: 'Get a user',
|
||||
},
|
||||
// Get All Operation
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many users',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: BASE_API_URL,
|
||||
qs: { search: '={{$parameter["searchQuery"]}}' },
|
||||
returnFullResponse: true,
|
||||
},
|
||||
output: {
|
||||
postReceive: [simplifyGetAllResponse],
|
||||
},
|
||||
send: {
|
||||
paginate: true,
|
||||
},
|
||||
operations: {
|
||||
pagination: getCursorPaginator(),
|
||||
},
|
||||
},
|
||||
action: 'Get many users',
|
||||
},
|
||||
// Update Operation
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update an existing user',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '={{"/api/v1/users/" + $parameter["userId"]}}',
|
||||
returnFullResponse: true,
|
||||
},
|
||||
},
|
||||
action: 'Update a user',
|
||||
},
|
||||
],
|
||||
default: 'getAll',
|
||||
},
|
||||
];
|
||||
const mainProfileFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. Nathan',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.firstName',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. Smith',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.lastName',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Username',
|
||||
name: 'login',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. nathan@example.com',
|
||||
hint: 'Unique identifier for the user, must be an email',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.login',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. nathan@example.com',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.email',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const createFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.city',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Cost Center',
|
||||
name: 'costCenter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.costCenter',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Country Code',
|
||||
name: 'countryCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.countryCode',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Department',
|
||||
name: 'department',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.department',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Display Name',
|
||||
name: 'displayName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.displayName',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Division',
|
||||
name: 'division',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.division',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Employee Number',
|
||||
name: 'employeeNumber',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.employeeNumber',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Honorific Prefix',
|
||||
name: 'honorificPrefix',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.honorificPrefix',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Honorific Suffix',
|
||||
name: 'honorificSuffix',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.honorificSuffix',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Locale',
|
||||
name: 'locale',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.locale',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Manager',
|
||||
name: 'manager',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.manager',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ManagerId',
|
||||
name: 'managerId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.managerId',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Middle Name',
|
||||
name: 'middleName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.middleName',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Mobile Phone',
|
||||
name: 'mobilePhone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.mobilePhone',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Nick Name',
|
||||
name: 'nickName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.nickName',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string',
|
||||
typeOptions: { password: true },
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'credentials.password.value',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Organization',
|
||||
name: 'organization',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.organization',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Address',
|
||||
name: 'postalAddress',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.postalAddress',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Preferred Language',
|
||||
name: 'preferredLanguage',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.preferredLanguage',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Primary Phone',
|
||||
name: 'primaryPhone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.primaryPhone',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Profile Url',
|
||||
name: 'profileUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.profileUrl',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Recovery Question Answer',
|
||||
name: 'recoveryQuestionAnswer',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'credentials.recovery_question.answer',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Recovery Question Question',
|
||||
name: 'recoveryQuestionQuestion',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'credentials.recovery_question.question',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Second Email',
|
||||
name: 'secondEmail',
|
||||
type: 'string',
|
||||
typeOptions: { email: true },
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.secondEmail',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.state',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Street Address',
|
||||
name: 'streetAddress',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.streetAddress',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Timezone',
|
||||
name: 'timezone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.timezone',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.title',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'User Type',
|
||||
name: 'userType',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.userType',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Zip Code',
|
||||
name: 'zipCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.zipCode',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const updateFields: INodeProperties[] = createFields
|
||||
.concat(mainProfileFields)
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
|
||||
export const userFields: INodeProperties[] = [
|
||||
// Fields for 'get', 'update', and 'delete' operations
|
||||
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'userId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'getUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By username',
|
||||
name: 'login',
|
||||
type: 'string',
|
||||
placeholder: '',
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. 00u1abcd2345EfGHIjk6',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['get', 'update', 'delete'],
|
||||
},
|
||||
},
|
||||
description: 'The user you want to operate on. Choose from the list, or specify an ID.',
|
||||
},
|
||||
// Fields specific to 'create' operation
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
placeholder: 'e.g. Nathan',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.firstName',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
placeholder: 'e.g. Smith',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.lastName',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Username',
|
||||
name: 'login',
|
||||
type: 'string',
|
||||
required: true,
|
||||
placeholder: 'e.g. nathan@example.com',
|
||||
hint: 'Unique identifier for the user, must be an email',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.login',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
required: true,
|
||||
placeholder: 'e.g. nathan@example.com',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
property: 'profile.email',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Activate',
|
||||
name: 'activate',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
description: 'Whether to activate the user and allow access to all assigned applications',
|
||||
},
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'getCreateFields',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add field',
|
||||
options: createFields,
|
||||
},
|
||||
|
||||
// Fields for 'update' operations
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'getUpdateFields',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add field',
|
||||
options: updateFields,
|
||||
},
|
||||
|
||||
// Fields specific to 'getAll' operation
|
||||
{
|
||||
displayName: 'Search Query',
|
||||
name: 'searchQuery',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. profile.lastName sw "Smi"',
|
||||
hint: 'Filter users by using the allowed syntax. <a href="https://developer.okta.com/docs/reference/core-okta-api/#filter" target="_blank">More info</a>.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
request: {
|
||||
qs: {
|
||||
prefix: '={{$value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 200,
|
||||
},
|
||||
default: 20,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'limit',
|
||||
},
|
||||
output: {
|
||||
maxResults: '={{$value}}', // Set maxResults to the value of current parameter
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
// Fields for 'get' and 'getAll' operations
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['get', 'getAll'],
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
// Fields specific to 'delete' operation
|
||||
{
|
||||
displayName: 'Send Email',
|
||||
name: 'sendEmail',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to send a deactivation email to the administrator',
|
||||
},
|
||||
];
|
170
packages/nodes-base/nodes/Okta/UserFunctions.ts
Normal file
170
packages/nodes-base/nodes/Okta/UserFunctions.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
import type {
|
||||
DeclarativeRestApiSettings,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecutePaginationFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestMethods,
|
||||
IHttpRequestOptions,
|
||||
ILoadOptionsFunctions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
INodeListSearchResult,
|
||||
INodePropertyOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
type OktaUser = {
|
||||
status: string;
|
||||
created: string;
|
||||
activated: string;
|
||||
lastLogin: string;
|
||||
lastUpdated: string;
|
||||
passwordChanged: string;
|
||||
profile: {
|
||||
login: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
id: string;
|
||||
};
|
||||
|
||||
export async function oktaApiRequest(
|
||||
this: IExecuteFunctions | IExecuteSingleFunctions | IHookFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: IDataObject = {},
|
||||
qs: IDataObject = {},
|
||||
url?: string,
|
||||
option: IDataObject = {},
|
||||
): Promise<OktaUser[]> {
|
||||
const credentials = await this.getCredentials('oktaApi');
|
||||
const baseUrl = `${credentials.url as string}/api/v1/${resource}`;
|
||||
const options: IHttpRequestOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
body: Object.keys(body).length ? body : undefined,
|
||||
qs: Object.keys(qs).length ? qs : undefined,
|
||||
url: url ?? baseUrl,
|
||||
json: true,
|
||||
...option,
|
||||
};
|
||||
return await (this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
'oktaApi',
|
||||
options,
|
||||
) as Promise<OktaUser[]>);
|
||||
}
|
||||
|
||||
export async function getUsers(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const responseData: OktaUser[] = await oktaApiRequest.call(this, 'GET', '/users/');
|
||||
const filteredUsers = responseData.filter((user) => {
|
||||
if (!filter) return true;
|
||||
const username = `${user.profile.login}`.toLowerCase();
|
||||
return username.includes(filter.toLowerCase());
|
||||
});
|
||||
const users: INodePropertyOptions[] = filteredUsers.map((user) => ({
|
||||
name: `${user.profile.login}`,
|
||||
value: user.id,
|
||||
}));
|
||||
return {
|
||||
results: users,
|
||||
};
|
||||
}
|
||||
|
||||
function simplifyOktaUser(item: OktaUser): IDataObject {
|
||||
return {
|
||||
id: item.id,
|
||||
status: item.status,
|
||||
created: item.created,
|
||||
activated: item.activated,
|
||||
lastLogin: item.lastLogin,
|
||||
lastUpdated: item.lastUpdated,
|
||||
passwordChanged: item.passwordChanged,
|
||||
profile: {
|
||||
firstName: item.profile.firstName,
|
||||
lastName: item.profile.lastName,
|
||||
login: item.profile.login,
|
||||
email: item.profile.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function simplifyGetAllResponse(
|
||||
this: IExecuteSingleFunctions,
|
||||
items: INodeExecutionData[],
|
||||
_response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
if (items.length === 0) return items;
|
||||
const simplify = this.getNodeParameter('simplify');
|
||||
if (!simplify)
|
||||
return ((items[0].json as unknown as IDataObject[]) ?? []).map((item: IDataObject) => ({
|
||||
json: item,
|
||||
headers: _response.headers,
|
||||
})) as INodeExecutionData[];
|
||||
let simplifiedItems: INodeExecutionData[] = [];
|
||||
if (items[0].json) {
|
||||
const jsonArray = items[0].json as unknown;
|
||||
simplifiedItems = (jsonArray as OktaUser[]).map((item: OktaUser) => {
|
||||
const simplifiedItem = simplifyOktaUser(item);
|
||||
return {
|
||||
json: simplifiedItem,
|
||||
headers: _response.headers,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return simplifiedItems;
|
||||
}
|
||||
|
||||
export async function simplifyGetResponse(
|
||||
this: IExecuteSingleFunctions,
|
||||
items: INodeExecutionData[],
|
||||
_response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const simplify = this.getNodeParameter('simplify');
|
||||
if (!simplify) return items;
|
||||
const item = items[0].json as OktaUser;
|
||||
const simplifiedItem = simplifyOktaUser(item);
|
||||
|
||||
return [
|
||||
{
|
||||
json: simplifiedItem,
|
||||
},
|
||||
] as INodeExecutionData[];
|
||||
}
|
||||
|
||||
export const getCursorPaginator = () => {
|
||||
return async function cursorPagination(
|
||||
this: IExecutePaginationFunctions,
|
||||
requestOptions: DeclarativeRestApiSettings.ResultOptions,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
if (!requestOptions.options.qs) {
|
||||
requestOptions.options.qs = {};
|
||||
}
|
||||
|
||||
let items: INodeExecutionData[] = [];
|
||||
let responseData: INodeExecutionData[];
|
||||
let nextCursor: string | undefined = undefined;
|
||||
const returnAll = this.getNodeParameter('returnAll', true) as boolean;
|
||||
do {
|
||||
requestOptions.options.qs.limit = 200;
|
||||
requestOptions.options.qs.after = nextCursor;
|
||||
responseData = await this.makeRoutingRequest(requestOptions);
|
||||
if (responseData.length > 0) {
|
||||
const headers = responseData[responseData.length - 1].headers;
|
||||
const headersLink = (headers as IDataObject)?.link as string | undefined;
|
||||
nextCursor = headersLink?.split('after=')[1]?.split('&')[0]?.split('>')[0];
|
||||
}
|
||||
items = items.concat(responseData);
|
||||
} while (returnAll && nextCursor);
|
||||
|
||||
return items;
|
||||
};
|
||||
};
|
375
packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts
Normal file
375
packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts
Normal file
|
@ -0,0 +1,375 @@
|
|||
import type {
|
||||
DeclarativeRestApiSettings,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecutePaginationFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
getCursorPaginator,
|
||||
getUsers,
|
||||
oktaApiRequest,
|
||||
simplifyGetAllResponse,
|
||||
simplifyGetResponse,
|
||||
} from '../UserFunctions';
|
||||
|
||||
describe('oktaApiRequest', () => {
|
||||
const mockGetCredentials = jest.fn();
|
||||
const mockHttpRequestWithAuthentication = jest.fn();
|
||||
|
||||
const mockContext = {
|
||||
getCredentials: mockGetCredentials,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
|
||||
},
|
||||
} as unknown as IExecuteFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetCredentials.mockClear();
|
||||
mockHttpRequestWithAuthentication.mockClear();
|
||||
});
|
||||
|
||||
it('should make a GET request and return data', async () => {
|
||||
mockGetCredentials.mockResolvedValue({ url: 'https://okta.example.com' });
|
||||
mockHttpRequestWithAuthentication.mockResolvedValue([
|
||||
{ profile: { firstName: 'John', lastName: 'Doe' }, id: '1' },
|
||||
]);
|
||||
|
||||
const response = await oktaApiRequest.call(mockContext, 'GET', 'users');
|
||||
|
||||
expect(mockGetCredentials).toHaveBeenCalledWith('oktaApi');
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('oktaApi', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'GET',
|
||||
body: undefined,
|
||||
qs: undefined,
|
||||
url: 'https://okta.example.com/api/v1/users',
|
||||
json: true,
|
||||
});
|
||||
expect(response).toEqual([{ profile: { firstName: 'John', lastName: 'Doe' }, id: '1' }]);
|
||||
});
|
||||
|
||||
// Tests for error handling
|
||||
it('should handle errors from oktaApiRequest', async () => {
|
||||
mockHttpRequestWithAuthentication.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(oktaApiRequest.call(mockContext, 'GET', 'users')).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUsers', () => {
|
||||
const mockOktaApiRequest = jest.fn();
|
||||
const mockContext = {
|
||||
getCredentials: jest.fn().mockResolvedValue({ url: 'https://okta.example.com' }),
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockOktaApiRequest,
|
||||
},
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOktaApiRequest.mockClear();
|
||||
});
|
||||
|
||||
it('should return users with filtering', async () => {
|
||||
mockOktaApiRequest.mockResolvedValue([
|
||||
{ profile: { login: 'John@example.com' }, id: '1' },
|
||||
{ profile: { login: 'Jane@example.com' }, id: '2' },
|
||||
]);
|
||||
|
||||
const response = await getUsers.call(mockContext, 'john');
|
||||
|
||||
expect(response).toEqual({
|
||||
results: [{ name: 'John@example.com', value: '1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all users when no filter is applied', async () => {
|
||||
mockOktaApiRequest.mockResolvedValue([
|
||||
{ profile: { login: 'John@example.com' }, id: '1' },
|
||||
{ profile: { login: 'Jane@example.com' }, id: '2' },
|
||||
]);
|
||||
|
||||
const response = await getUsers.call(mockContext);
|
||||
|
||||
expect(response).toEqual({
|
||||
results: [
|
||||
{ name: 'John@example.com', value: '1' },
|
||||
{ name: 'Jane@example.com', value: '2' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for empty results
|
||||
it('should handle empty results from oktaApiRequest', async () => {
|
||||
mockOktaApiRequest.mockResolvedValue([]);
|
||||
|
||||
const response = await getUsers.call(mockContext);
|
||||
|
||||
expect(response).toEqual({
|
||||
results: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('simplifyGetAllResponse', () => {
|
||||
const mockGetNodeParameter = jest.fn();
|
||||
const mockContext = {
|
||||
getNodeParameter: mockGetNodeParameter,
|
||||
} as unknown as IExecuteSingleFunctions;
|
||||
const mockResponse = jest.fn() as unknown as IN8nHttpFullResponse;
|
||||
|
||||
const items: INodeExecutionData[] = [
|
||||
{
|
||||
json: [
|
||||
{
|
||||
id: '01',
|
||||
status: 'ACTIVE',
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
activated: '2023-01-01T00:00:01.000Z',
|
||||
lastLogin: null,
|
||||
lastUpdated: '2023-01-01T00:00:01.000Z',
|
||||
passwordChanged: null,
|
||||
some_item: 'some_value',
|
||||
profile: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
login: 'john.doe@example.com',
|
||||
email: 'john.doe@example.com',
|
||||
some_profile_item: 'some_profile_value',
|
||||
},
|
||||
},
|
||||
] as unknown as IDataObject,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetNodeParameter.mockClear();
|
||||
});
|
||||
|
||||
it('should return items unchanged when simplify parameter is not set', async () => {
|
||||
mockGetNodeParameter.mockReturnValueOnce(false);
|
||||
|
||||
const expectedResult: INodeExecutionData[] = [
|
||||
{
|
||||
json: {
|
||||
id: '01',
|
||||
status: 'ACTIVE',
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
activated: '2023-01-01T00:00:01.000Z',
|
||||
lastLogin: null,
|
||||
lastUpdated: '2023-01-01T00:00:01.000Z',
|
||||
passwordChanged: null,
|
||||
some_item: 'some_value',
|
||||
profile: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
login: 'john.doe@example.com',
|
||||
email: 'john.doe@example.com',
|
||||
some_profile_item: 'some_profile_value',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = await simplifyGetAllResponse.call(mockContext, items, mockResponse);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should simplify items correctly', async () => {
|
||||
mockGetNodeParameter.mockReturnValueOnce(true);
|
||||
|
||||
const expectedResult: INodeExecutionData[] = [
|
||||
{
|
||||
json: {
|
||||
id: '01',
|
||||
status: 'ACTIVE',
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
activated: '2023-01-01T00:00:01.000Z',
|
||||
lastLogin: null,
|
||||
lastUpdated: '2023-01-01T00:00:01.000Z',
|
||||
passwordChanged: null,
|
||||
profile: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
login: 'john.doe@example.com',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = await simplifyGetAllResponse.call(mockContext, items, mockResponse);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should return an empty array when items is an empty array', async () => {
|
||||
mockGetNodeParameter.mockReturnValueOnce(false);
|
||||
|
||||
const emptyArrayItems: INodeExecutionData[] = [];
|
||||
|
||||
const result = await simplifyGetAllResponse.call(mockContext, emptyArrayItems, mockResponse);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('simplifyGetResponse', () => {
|
||||
const mockGetNodeParameter = jest.fn();
|
||||
const mockContext = {
|
||||
getNodeParameter: mockGetNodeParameter,
|
||||
} as unknown as IExecuteSingleFunctions;
|
||||
const mockResponse = jest.fn() as unknown as IN8nHttpFullResponse;
|
||||
|
||||
const items: INodeExecutionData[] = [
|
||||
{
|
||||
json: {
|
||||
id: '01',
|
||||
status: 'ACTIVE',
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
activated: '2023-01-01T00:00:01.000Z',
|
||||
lastLogin: null,
|
||||
lastUpdated: '2023-01-01T00:00:01.000Z',
|
||||
passwordChanged: null,
|
||||
some_item: 'some_value',
|
||||
profile: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
login: 'john.doe@example.com',
|
||||
email: 'john.doe@example.com',
|
||||
some_profile_item: 'some_profile_value',
|
||||
},
|
||||
} as unknown as IDataObject,
|
||||
},
|
||||
];
|
||||
beforeEach(() => {
|
||||
mockGetNodeParameter.mockClear();
|
||||
});
|
||||
|
||||
it('should return the item unchanged when simplify parameter is not set', async () => {
|
||||
mockGetNodeParameter.mockReturnValueOnce(false);
|
||||
|
||||
const expectedResult: INodeExecutionData[] = [
|
||||
{
|
||||
json: {
|
||||
id: '01',
|
||||
status: 'ACTIVE',
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
activated: '2023-01-01T00:00:01.000Z',
|
||||
lastLogin: null,
|
||||
lastUpdated: '2023-01-01T00:00:01.000Z',
|
||||
passwordChanged: null,
|
||||
some_item: 'some_value',
|
||||
profile: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
login: 'john.doe@example.com',
|
||||
email: 'john.doe@example.com',
|
||||
some_profile_item: 'some_profile_value',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = await simplifyGetResponse.call(mockContext, items, mockResponse);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should simplify the item correctly', async () => {
|
||||
mockGetNodeParameter.mockReturnValueOnce(true);
|
||||
|
||||
const expectedResult: INodeExecutionData[] = [
|
||||
{
|
||||
json: {
|
||||
id: '01',
|
||||
status: 'ACTIVE',
|
||||
created: '2023-01-01T00:00:00.000Z',
|
||||
activated: '2023-01-01T00:00:01.000Z',
|
||||
lastLogin: null,
|
||||
lastUpdated: '2023-01-01T00:00:01.000Z',
|
||||
passwordChanged: null,
|
||||
profile: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
login: 'john.doe@example.com',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = await simplifyGetResponse.call(mockContext, items, mockResponse);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
describe('getCursorPaginator', () => {
|
||||
let mockContext: IExecutePaginationFunctions;
|
||||
let mockRequestOptions: DeclarativeRestApiSettings.ResultOptions;
|
||||
const baseUrl = 'https://api.example.com';
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = {
|
||||
getNodeParameter: jest.fn(),
|
||||
makeRoutingRequest: jest.fn(),
|
||||
} as unknown as IExecutePaginationFunctions;
|
||||
|
||||
mockRequestOptions = {
|
||||
options: {
|
||||
qs: {},
|
||||
},
|
||||
} as DeclarativeRestApiSettings.ResultOptions;
|
||||
});
|
||||
|
||||
it('should return all items when returnAll is true', async () => {
|
||||
const mockResponseData: INodeExecutionData[] = [
|
||||
{ json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } },
|
||||
{ json: { id: 2 }, headers: { link: `<${baseUrl}?after=cursor2>` } },
|
||||
{ json: { id: 3 }, headers: { link: `<${baseUrl}>` } },
|
||||
];
|
||||
|
||||
(mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
|
||||
(mockContext.makeRoutingRequest as jest.Mock)
|
||||
.mockResolvedValueOnce([mockResponseData[0]])
|
||||
.mockResolvedValueOnce([mockResponseData[1]])
|
||||
.mockResolvedValueOnce([mockResponseData[2]]);
|
||||
|
||||
const paginator = getCursorPaginator().bind(mockContext);
|
||||
const result = await paginator(mockRequestOptions);
|
||||
|
||||
expect(result).toEqual(mockResponseData);
|
||||
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
|
||||
expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should return items until nextCursor is undefined', async () => {
|
||||
const mockResponseData: INodeExecutionData[] = [
|
||||
{ json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } },
|
||||
{ json: { id: 2 }, headers: { link: `<${baseUrl}>` } },
|
||||
];
|
||||
|
||||
(mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
|
||||
(mockContext.makeRoutingRequest as jest.Mock)
|
||||
.mockResolvedValueOnce([mockResponseData[0]])
|
||||
.mockResolvedValueOnce([mockResponseData[1]]);
|
||||
|
||||
const paginator = getCursorPaginator().bind(mockContext);
|
||||
const result = await paginator(mockRequestOptions);
|
||||
|
||||
expect(result).toEqual(mockResponseData);
|
||||
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
|
||||
expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle empty response data', async () => {
|
||||
(mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
|
||||
(mockContext.makeRoutingRequest as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const paginator = getCursorPaginator().bind(mockContext);
|
||||
const result = await paginator(mockRequestOptions);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
|
||||
expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -648,6 +648,7 @@
|
|||
"dist/nodes/Notion/NotionTrigger.node.js",
|
||||
"dist/nodes/Npm/Npm.node.js",
|
||||
"dist/nodes/Odoo/Odoo.node.js",
|
||||
"dist/nodes/Okta/Okta.node.js",
|
||||
"dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
|
||||
"dist/nodes/OpenAi/OpenAi.node.js",
|
||||
"dist/nodes/OpenThesaurus/OpenThesaurus.node.js",
|
||||
|
|
|
@ -308,6 +308,7 @@ export interface ICredentialTestRequestData {
|
|||
type ICredentialHttpRequestNode = {
|
||||
name: string;
|
||||
docsUrl: string;
|
||||
hidden?: boolean;
|
||||
} & ({ apiBaseUrl: string } | { apiBaseUrlPlaceholder: string });
|
||||
|
||||
export interface ICredentialType {
|
||||
|
|
Loading…
Reference in a new issue