Changes after feedback

This commit is contained in:
Adina Totorean 2024-12-05 21:15:33 +02:00
parent 477bbbc7d8
commit 3615cd9021
3 changed files with 143 additions and 339 deletions

View file

@ -14,16 +14,6 @@ import type {
} from 'n8n-workflow';
import { ApplicationError, jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow';
/* Function which helps while developing the node */
// ToDo: Remove before completing the pull request
export async function presendTest(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
console.log('requestOptions', requestOptions);
return requestOptions;
}
/*
* Helper function which stringifies the body before sending the request.
* It is added to the routing property in the "resource" parameter thus for all requests.
@ -47,31 +37,73 @@ export async function presendFilter(
let filterType = additionalFields.filterType as string;
const filterValue = additionalFields.filterValue as string;
if (filterAttribute && filterType && filterValue) {
// Convert the filterType to the format the API expects
const filterTypeMapping: { [key: string]: string } = {
exactMatch: '=',
startsWith: '^=',
};
filterType = filterTypeMapping[filterType] || filterType;
console.log('Attribute', filterAttribute, 'Type', filterType, 'Value', filterValue);
// Parse the body if it's a string to add the new property
let body: IDataObject;
if (typeof requestOptions.body === 'string') {
try {
body = JSON.parse(requestOptions.body) as IDataObject;
} catch (error) {
throw new NodeOperationError(this.getNode(), 'Failed to parse requestOptions body');
}
} else {
body = requestOptions.body as IDataObject;
if (!filterAttribute || !filterType || !filterValue) {
throw new NodeOperationError(
this.getNode(),
'Please provide Filter Attribute, Filter Type, and Filter Value to use filtering.',
);
}
const filterTypeMapping: { [key: string]: string } = {
exactMatch: '=',
startsWith: '^=',
};
filterType = filterTypeMapping[filterType] || filterType;
let body: IDataObject;
if (typeof requestOptions.body === 'string') {
try {
body = JSON.parse(requestOptions.body) as IDataObject;
} catch (error) {
throw new NodeOperationError(this.getNode(), 'Failed to parse requestOptions body');
}
requestOptions.body = JSON.stringify({
...body,
Filter: `${filterAttribute} ${filterType} "${filterValue}"`,
});
} else {
body = requestOptions.body as IDataObject;
}
requestOptions.body = JSON.stringify({
...body,
Filter: `${filterAttribute} ${filterType} "${filterValue}"`,
});
return requestOptions;
}
export async function presendOptions(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const options = this.getNodeParameter('options', {}) as IDataObject;
const hasOptions = options.Description || options.Precedence || options.Path || options.RoleArn;
if (!hasOptions) {
throw new NodeOperationError(
this.getNode(),
'At least one of the options (Description, Precedence, Path, or RoleArn) must be provided to update the group.',
);
}
return requestOptions;
}
export async function presendPath(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const path = this.getNodeParameter('path', '/') as string;
if (path.length < 1 || path.length > 512) {
throw new NodeOperationError(this.getNode(), 'Path must be between 1 and 512 characters.');
}
if (!/^\/$|^\/[\u0021-\u007E]+\/$/.test(path)) {
throw new NodeOperationError(
this.getNode(),
'Path must begin and end with a forward slash and contain valid ASCII characters.',
);
}
return requestOptions;
@ -176,34 +208,33 @@ export async function handleErrorPostReceive(
const errorType = responseBody.__type ?? response.headers?.['x-amzn-errortype'];
const errorMessage = responseBody.message ?? response.headers?.['x-amzn-errormessage'];
// Resource/Operation specific errors
if (resource === 'group') {
if (operation === 'delete') {
if (errorType === 'ResourceNotFoundException' || errorType === 'NoSuchEntity') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group' and try again",
message: 'The group you are deleting could not be found.',
description: 'Adjust the "Group" parameter setting to delete the group correctly.',
});
}
} else if (operation === 'get') {
if (errorType === 'ResourceNotFoundException' || errorType === 'NoSuchEntity') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group' and try again",
message: 'The group you are requesting could not be found.',
description: 'Adjust the "Group" parameter setting to retrieve the group correctly.',
});
}
} else if (operation === 'update') {
if (errorType === 'ResourceNotFoundException' || errorType === 'NoSuchEntity') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group' and try again",
message: 'The group you are updating could not be found.',
description: 'Adjust the "Group" parameter setting to update the group correctly.',
});
}
} else if (operation === 'create') {
if (errorType === 'EntityAlreadyExists' || errorType === 'GroupExistsException') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'The group is already created',
description: "Double-check the value in the parameter 'Group Name' and try again",
message: 'The group you are trying to create already exists',
description: 'Adjust the "Group Name" parameter setting to create the group correctly.',
});
}
}
@ -214,19 +245,18 @@ export async function handleErrorPostReceive(
errorMessage === 'User account already exists'
) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'The user is already created',
description: "Double-check the value in the parameter 'User Name' and try again",
message: 'The user you are trying to create already exists',
description: 'Adjust the "User Name" parameter setting to create the user correctly.',
});
}
} else if (operation === 'addToGroup') {
// Group or user doesn't exist
if (errorType === 'UserNotFoundException') {
const user = this.getNodeParameter('user.value', '') as string;
if (typeof errorMessage === 'string' && errorMessage.includes(user)) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User' and try again.",
message: 'The user you are requesting could not be found.',
description: 'Adjust the "User" parameter setting to retrieve the post correctly.',
});
}
} else if (errorType === 'ResourceNotFoundException') {
@ -234,34 +264,33 @@ export async function handleErrorPostReceive(
if (typeof errorMessage === 'string' && errorMessage.includes(group)) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group' and try again.",
message: 'The group you are requesting could not be found.',
description: 'Adjust the "Group" parameter setting to retrieve the post correctly.',
});
}
}
} else if (operation === 'delete') {
if (errorType === 'UserNotFoundException') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User' and try again",
message: 'The user you are requesting could not be found.',
description: 'Adjust the "User" parameter setting to retrieve the post correctly.',
});
}
} else if (operation === 'get') {
if (errorType === 'UserNotFoundException') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User' and try again",
message: 'The user you are requesting could not be found.',
description: 'Adjust the "User" parameter setting to retrieve the post correctly.',
});
}
} else if (operation === 'removeFromGroup') {
// Group or user doesn't exist
if (errorType === 'UserNotFoundException') {
const user = this.getNodeParameter('user.value', '') as string;
if (typeof errorMessage === 'string' && errorMessage.includes(user)) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User' and try again.",
message: 'The user you are deleting could not be found.',
description: 'Adjust the "User" parameter setting to delete the user correctly.',
});
}
} else if (errorType === 'ResourceNotFoundException') {
@ -269,72 +298,21 @@ export async function handleErrorPostReceive(
if (typeof errorMessage === 'string' && errorMessage.includes(group)) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group' and try again.",
message: 'The group you are requesting could not be found.',
description: 'Adjust the "Group" parameter setting to delete the user correctly.',
});
}
}
} else if (operation === 'update') {
if (errorType === 'UserNotFoundException') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User' and try again",
message: 'The user you are updating could not be found.',
description: 'Adjust the "User" parameter setting to update the user correctly.',
});
}
}
}
// Generic Error Handling
if (errorType === 'InvalidParameterException') {
const group = this.getNodeParameter('group.value', '') as string;
const parameterResource =
resource === 'group' || (typeof errorMessage === 'string' && errorMessage.includes(group))
? 'group'
: 'user';
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: `The ${parameterResource} ID is invalid`,
description: 'The ID should be in the format e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315',
});
}
if (errorType === 'InternalErrorException') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'Internal Server Error',
description: 'Amazon Cognito encountered an internal error. Try again later.',
});
}
if (errorType === 'TooManyRequestsException') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'Too Many Requests',
description: 'You have exceeded the allowed number of requests. Try again later.',
});
}
if (errorType === 'NotAuthorizedException') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'Unauthorized Access',
description:
'You are not authorized to perform this operation. Check your permissions and try again.',
});
}
if (errorType === 'ServiceFailure') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'Service Failure',
description:
'The request processing has failed because of an unknown error, exception, or failure. Try again later.',
});
}
if (errorType === 'LimitExceeded') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'Limit Exceeded',
description:
'The request was rejected because it attempted to create resources beyond the current AWS account limits. Check your AWS limits and try again.',
});
}
throw new NodeApiError(this.getNode(), response as unknown as JsonObject);
}
@ -401,13 +379,13 @@ export async function searchUserPools(
paginationToken?: string,
): Promise<INodeListSearchResult> {
const opts: IHttpRequestOptions = {
url: '', // the base url is set in "awsRequest"
url: '',
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.ListUserPools',
},
body: JSON.stringify({
MaxResults: 60, // the maximum number by documentation is 60
MaxResults: 60,
NextToken: paginationToken ?? undefined,
}),
};
@ -432,7 +410,7 @@ export async function searchUserPools(
return 0;
});
return { results, paginationToken: responseData.NextToken }; // ToDo: Test if pagination for the search methods works
return { results, paginationToken: responseData.NextToken };
}
export async function searchUsers(
@ -440,20 +418,16 @@ export async function searchUsers(
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
// Get the userPoolId from the input
const userPoolIdRaw = this.getNodeParameter('userPoolId', '') as IDataObject;
// Extract the actual value
const userPoolId = userPoolIdRaw.value as string;
// Ensure that userPoolId is provided
if (!userPoolId) {
throw new ApplicationError('User Pool ID is required to search users');
}
// Setup the options for the AWS request
const opts: IHttpRequestOptions = {
url: '', // the base URL is set in "awsRequest"
url: '',
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.ListUsers',
@ -465,30 +439,23 @@ export async function searchUsers(
}),
};
// Make the AWS request
const responseData: IDataObject = await awsRequest.call(this, opts);
// Extract users from the response
const users = responseData.Users as IDataObject[] | undefined;
// Handle cases where no users are returned
if (!users) {
console.warn('No users found in the response');
return { results: [] };
}
// Map and filter the response data to create results
const results: INodeListSearchItems[] = users
.map((user) => {
// Extract user attributes, if any
const attributes = user.Attributes as Array<{ Name: string; Value: string }> | undefined;
// Find the `email` or `sub` attribute, fallback to `Username`
const email = attributes?.find((attr) => attr.Name === 'email')?.Value;
const sub = attributes?.find((attr) => attr.Name === 'sub')?.Value;
const username = user.Username as string;
// Use email, sub, or Username as the user name and value
const name = email || sub || username;
const value = username;
@ -504,7 +471,6 @@ export async function searchUsers(
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
// Return the results and the pagination token
return { results, paginationToken: responseData.NextToken as string | undefined };
}
@ -513,17 +479,13 @@ export async function searchGroups(
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
// Get the userPoolId from the input
const userPoolIdRaw = this.getNodeParameter('userPoolId', '') as IDataObject;
// Extract the actual value
const userPoolId = userPoolIdRaw.value as string;
// Ensure that userPoolId is provided
if (!userPoolId) {
throw new ApplicationError('User Pool ID is required to search groups');
}
// Setup the options for the AWS request
const opts: IHttpRequestOptions = {
url: '',
method: 'POST',
@ -541,12 +503,10 @@ export async function searchGroups(
const groups = responseData.Groups as Array<{ GroupName?: string }> | undefined;
// If no groups exist, return an empty list
if (!groups) {
return { results: [] };
}
// Map and filter the response
const results: INodeListSearchItems[] = groups
.filter((group) => group.GroupName)
.map((group) => ({

View file

@ -1,7 +1,6 @@
import type { IExecuteSingleFunctions, IHttpRequestOptions, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { INodeProperties } from 'n8n-workflow';
import { handleErrorPostReceive, handlePagination } from '../GenericFunctions';
import { handleErrorPostReceive, handlePagination, presendPath } from '../GenericFunctions';
export const groupOperations: INodeProperties[] = [
{
@ -92,7 +91,7 @@ export const groupOperations: INodeProperties[] = [
},
qs: {
pageSize:
'={{ $parameter["limit"] ? ($parameter["limit"] < 60 ? $parameter["limit"] : 60) : 60 }}', // The API allows maximum 60 results per page
'={{ $parameter["limit"] ? ($parameter["limit"] < 60 ? $parameter["limit"] : 60) : 60 }}',
},
ignoreHttpStatusErrors: true,
},
@ -257,38 +256,13 @@ const createFields: INodeProperties[] = [
send: {
property: 'Path',
type: 'body',
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const path = this.getNodeParameter('path', '/') as string;
// Length validation
if (path.length < 1 || path.length > 512) {
throw new NodeOperationError(
this.getNode(),
'Path must be between 1 and 512 characters.',
);
}
// Regex validation
if (!/^\/$|^\/[\u0021-\u007E]+\/$/.test(path)) {
throw new NodeOperationError(
this.getNode(),
'Path must begin and end with a forward slash and contain valid ASCII characters.',
);
}
return requestOptions;
},
],
preSend: [presendPath],
},
},
},
{
displayName: 'Role ARN',
name: 'RoleArn',
name: 'Arn',
default: '',
placeholder: 'e.g. arn:aws:iam::123456789012:role/GroupRole',
description: 'The role ARN for the group, used for setting claims in tokens',
@ -296,7 +270,7 @@ const createFields: INodeProperties[] = [
routing: {
send: {
type: 'body',
property: 'RoleArn',
property: 'Arn',
},
},
},
@ -510,91 +484,9 @@ const getFields: INodeProperties[] = [
},
],
},
{
displayName: 'Include Members',
name: 'includeMembers',
type: 'boolean',
default: false,
description: 'Whether include members of the group in the result',
displayOptions: {
show: {
resource: ['group'],
operation: ['get'],
},
},
routing: {
send: {
property: '$expand',
type: 'query',
value:
'={{ $value ? "members($select=CreatedDate,Description,GroupName,LastModifiedDate,Precedence,UserPoolId)" : undefined }}',
},
},
},
{
displayName: 'Include Group Policy',
name: 'includeGroupPolicy',
type: 'boolean',
default: false,
description: 'Whether include group policy details in the result',
displayOptions: {
show: {
resource: ['group'],
operation: ['get'],
},
},
routing: {
send: {
property: '$expand',
type: 'query',
value: '={{ $value ? "groupPolicy($select=policyName,policyType)" : undefined }}',
},
},
},
{
displayName: 'Simplified',
name: 'simplified',
type: 'boolean',
default: false,
description: 'Whether simplify the response if there are more than 10 fields',
displayOptions: {
show: {
resource: ['group'],
operation: ['get'],
},
},
routing: {
send: {
property: '$select',
type: 'query',
value: 'CreatedDate,Description,GroupName,LastModifiedDate,Precedence,UserPoolId',
},
},
},
];
const getAllFields: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: { show: { resource: ['group'], operation: ['getAll'] } },
type: 'boolean',
},
{
displayName: 'Limit',
name: 'limit',
required: true,
type: 'number',
typeOptions: {
minValue: 1,
},
default: 20,
description: 'Max number of results to return',
displayOptions: { show: { resource: ['group'], operation: ['getAll'], returnAll: [false] } },
routing: { send: { type: 'body', property: 'Limit' } },
},
{
displayName: 'User Pool ID',
name: 'userPoolId',
@ -633,65 +525,25 @@ const getAllFields: INodeProperties[] = [
],
},
{
displayName: 'Include Members',
name: 'includeMembers',
type: 'boolean',
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether include members of the group in the result',
displayOptions: {
show: {
resource: ['group'],
operation: ['getAll'],
},
},
routing: {
send: {
property: '$expand',
type: 'query',
value:
'={{ $value ? "members($select=CreatedDate,Description,GroupName,LastModifiedDate,Precedence,UserPoolId)" : undefined }}',
},
},
description: 'Whether to return all results or only up to a given limit',
displayOptions: { show: { resource: ['group'], operation: ['getAll'] } },
type: 'boolean',
},
{
displayName: 'Include Group Policy',
name: 'includeGroupPolicy',
type: 'boolean',
default: false,
description: 'Whether include group policy details in the result',
displayOptions: {
show: {
resource: ['group'],
operation: ['getAll'],
},
},
routing: {
send: {
property: '$expand',
type: 'query',
value: '={{ $value ? "groupPolicy($select=policyName,policyType)" : undefined }}',
},
},
},
{
displayName: 'Simplified',
name: 'simplified',
type: 'boolean',
default: false,
description: 'Whether simplify the response if there are more than 10 fields',
displayOptions: {
show: {
resource: ['group'],
operation: ['getAll'],
},
},
routing: {
send: {
property: '$select',
type: 'query',
value: 'CreatedDate,Description,GroupName,LastModifiedDate,Precedence,UserPoolId',
},
displayName: 'Limit',
name: 'limit',
required: true,
type: 'number',
typeOptions: {
minValue: 1,
},
default: 20,
description: 'Max number of results to return',
displayOptions: { show: { resource: ['group'], operation: ['getAll'], returnAll: [false] } },
routing: { send: { type: 'body', property: 'Limit' } },
},
];
@ -848,38 +700,13 @@ const updateFields: INodeProperties[] = [
send: {
property: 'Path',
type: 'body',
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const path = this.getNodeParameter('path', '/') as string;
// Length validation
if (path.length < 1 || path.length > 512) {
throw new NodeOperationError(
this.getNode(),
'Path must be between 1 and 512 characters.',
);
}
// Regex validation
if (!/^\/$|^\/[\u0021-\u007E]+\/$/.test(path)) {
throw new NodeOperationError(
this.getNode(),
'Path must begin and end with a forward slash and contain valid ASCII characters.',
);
}
return requestOptions;
},
],
preSend: [presendPath],
},
},
},
{
displayName: 'Role ARN',
name: 'RoleArn',
name: 'Arn',
default: '',
placeholder: 'e.g. arn:aws:iam::123456789012:role/GroupRole',
description:
@ -888,7 +715,7 @@ const updateFields: INodeProperties[] = [
routing: {
send: {
type: 'body',
property: 'RoleArn',
property: 'Arn',
},
},
},

View file

@ -34,7 +34,15 @@ export const userOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
postReceive: [
handleErrorPostReceive,
{
type: 'set',
properties: {
value: '={{ { "added": true } }}',
},
},
],
},
},
},
@ -114,7 +122,7 @@ export const userOperations: INodeProperties[] = [
},
qs: {
pageSize:
'={{ $parameter["limit"] ? ($parameter["limit"] < 60 ? $parameter["limit"] : 60) : 60 }}', // The API allows maximum 60 results per page
'={{ $parameter["limit"] ? ($parameter["limit"] < 60 ? $parameter["limit"] : 60) : 60 }}',
},
ignoreHttpStatusErrors: true,
},
@ -138,7 +146,15 @@ export const userOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
postReceive: [
handleErrorPostReceive,
{
type: 'set',
properties: {
value: '={{ { "removed": true } }}',
},
},
],
},
},
},
@ -262,7 +278,7 @@ const createFields: INodeProperties[] = [
displayName: 'Client Metadata',
name: 'clientMetadata',
type: 'fixedCollection',
placeholder: 'Add Metadata Pair',
placeholder: 'Add Metadata',
default: { metadata: [] },
description: 'A map of custom key-value pairs for workflows triggered by this action',
typeOptions: {
@ -318,7 +334,7 @@ const createFields: INodeProperties[] = [
name: 'MessageAction',
default: 'RESEND',
description:
"Set to RESEND to resend the invitation message to a user that already exists and reset the expiration limit on the user's account. Set to SUPPRESS to suppress sending the message. You can specify only one value.",
"Set to RESEND to resend the invitation message to a user that already exists and reset the expiration limit on the user's account. Set to SUPPRESS to suppress sending the message.",
type: 'options',
options: [
{
@ -685,7 +701,13 @@ const getAllFields: INodeProperties[] = [
name: 'filterAttribute',
type: 'options',
default: 'username',
hint: 'Make sure to select an attribute, type, and provide a value before submitting.',
description: 'The attribute to search for',
routing: {
send: {
preSend: [presendFilter],
},
},
options: [
{ name: 'Cognito User Status', value: 'cognito:user_status' },
{ name: 'Email', value: 'email' },
@ -716,11 +738,6 @@ const getAllFields: INodeProperties[] = [
type: 'string',
default: '',
description: 'The value of the attribute to search for',
routing: {
send: {
preSend: [presendFilter],
},
},
},
],
},