mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57: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 knownCredentials = loader.known.credentials;
|
||||||
const credentialTypes = Object.values(loader.credentialTypes).map((data) => {
|
const credentialTypes = Object.values(loader.credentialTypes).map((data) => {
|
||||||
const credentialType = data.type;
|
const credentialType = data.type;
|
||||||
if (knownCredentials[credentialType.name].supportedNodes?.length > 0) {
|
if (
|
||||||
delete credentialType.httpRequestNode;
|
knownCredentials[credentialType.name].supportedNodes?.length > 0 &&
|
||||||
|
credentialType.httpRequestNode
|
||||||
|
) {
|
||||||
|
credentialType.httpRequestNode.hidden = true;
|
||||||
}
|
}
|
||||||
return credentialType;
|
return credentialType;
|
||||||
});
|
});
|
||||||
|
|
|
@ -206,7 +206,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const httpOnlyCredentialTypes = computed(() => {
|
const httpOnlyCredentialTypes = computed(() => {
|
||||||
return allCredentialTypes.value.filter((credentialType) => credentialType.httpRequestNode);
|
return allCredentialTypes.value.filter(
|
||||||
|
(credentialType) => credentialType.httpRequestNode && !credentialType.httpRequestNode.hidden,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
|
@ -30,12 +30,13 @@ export class OktaApi implements ICredentialType {
|
||||||
placeholder: 'https://dev-123456.okta.com',
|
placeholder: 'https://dev-123456.okta.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'SSWS Access Token',
|
displayName: 'Access Token',
|
||||||
name: 'accessToken',
|
name: 'accessToken',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
typeOptions: { password: true },
|
typeOptions: { password: true },
|
||||||
required: true,
|
required: true,
|
||||||
default: '',
|
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/Notion/NotionTrigger.node.js",
|
||||||
"dist/nodes/Npm/Npm.node.js",
|
"dist/nodes/Npm/Npm.node.js",
|
||||||
"dist/nodes/Odoo/Odoo.node.js",
|
"dist/nodes/Odoo/Odoo.node.js",
|
||||||
|
"dist/nodes/Okta/Okta.node.js",
|
||||||
"dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
|
"dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
|
||||||
"dist/nodes/OpenAi/OpenAi.node.js",
|
"dist/nodes/OpenAi/OpenAi.node.js",
|
||||||
"dist/nodes/OpenThesaurus/OpenThesaurus.node.js",
|
"dist/nodes/OpenThesaurus/OpenThesaurus.node.js",
|
||||||
|
|
|
@ -308,6 +308,7 @@ export interface ICredentialTestRequestData {
|
||||||
type ICredentialHttpRequestNode = {
|
type ICredentialHttpRequestNode = {
|
||||||
name: string;
|
name: string;
|
||||||
docsUrl: string;
|
docsUrl: string;
|
||||||
|
hidden?: boolean;
|
||||||
} & ({ apiBaseUrl: string } | { apiBaseUrlPlaceholder: string });
|
} & ({ apiBaseUrl: string } | { apiBaseUrlPlaceholder: string });
|
||||||
|
|
||||||
export interface ICredentialType {
|
export interface ICredentialType {
|
||||||
|
|
Loading…
Reference in a new issue