Fixed some issues after feedback

This commit is contained in:
Adina Totorean 2025-01-06 15:38:03 +02:00
parent f3bcf18ce5
commit abdd1521c0
4 changed files with 456 additions and 210 deletions

View file

@ -14,6 +14,14 @@ import type {
} from 'n8n-workflow';
import { ApplicationError, jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow';
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.
@ -33,14 +41,20 @@ export async function presendFilter(
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
const filterAttribute = additionalFields.filterAttribute as string;
const filterAttribute = additionalFields.filters as string;
let filterType = additionalFields.filterType as string;
const filterValue = additionalFields.filterValue as string;
if (!filterAttribute || !filterType || !filterValue) {
const hasAnyFilter = filterAttribute || filterType || filterValue;
if (!hasAnyFilter) {
return requestOptions;
}
if (hasAnyFilter && (!filterAttribute || !filterType || !filterValue)) {
throw new NodeOperationError(
this.getNode(),
'Please provide Filter Attribute, Filter Type, and Filter Value to use filtering.',
'If filtering is used, please provide Filter Attribute, Filter Type, and Filter Value.',
);
}
@ -69,25 +83,25 @@ export async function presendFilter(
return requestOptions;
}
export async function presendOptions(
export async function presendAdditionalFields(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const options = this.getNodeParameter('options', {}) as IDataObject;
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
const hasOptions = options.Description || options.Precedence || options.Path || options.RoleArn;
const hasOptions = Object.keys(additionalFields).length > 0;
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.',
'At least one of the additional fields must be provided to update the group.',
);
}
return requestOptions;
}
export async function presendPath(
export async function presendVerifyPath(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
@ -325,7 +339,7 @@ export async function handleErrorPostReceive(
/* Helper function used in listSearch methods */
export async function awsRequest(
this: ILoadOptionsFunctions | IPollFunctions,
this: ILoadOptionsFunctions | IPollFunctions | IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IDataObject> {
const region = (await this.getCredentials('aws')).region as string;
@ -376,7 +390,6 @@ export async function awsRequest(
}
}
/* listSearch methods */
export async function searchUserPools(
this: ILoadOptionsFunctions,
filter?: string,
@ -448,7 +461,6 @@ export async function searchUsers(
const users = responseData.Users as IDataObject[] | undefined;
if (!users) {
console.warn('No users found in the response');
return { results: [] };
}
@ -529,3 +541,283 @@ export async function searchGroups(
return { results, paginationToken: responseData.NextToken };
}
export async function simplifyData(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const simple = this.getNodeParameter('simple') as boolean;
type UserPool = {
Arn: string;
CreationDate: number;
DeletionProtection: string;
Domain: string;
EstimatedNumberOfUsers: number;
Id: string;
LastModifiedDate: number;
MfaConfiguration: string;
Name: string;
};
type User = {
Enabled: boolean;
UserAttributes?: Array<{ Name: string; Value: string }>;
Attributes?: Array<{ Name: string; Value: string }>;
UserCreateDate: number;
UserLastModifiedDate: number;
UserStatus: string;
Username: string;
};
function mapUserAttributes(userAttributes: Array<{ Name: string; Value: string }>): {
[key: string]: string;
} {
return userAttributes?.reduce(
(acc, { Name, Value }) => {
if (Name !== 'sub') {
acc[Name] = Value;
}
return acc;
},
{} as { [key: string]: string },
);
}
if (!simple) {
return items;
}
const resource = this.getNodeParameter('resource');
const operation = this.getNodeParameter('operation');
const simplifiedItems = items
.map((item) => {
const data = item.json?.UserPool as UserPool | undefined;
const userData = item.json as User | undefined;
const users = item.json?.Users as User[] | undefined;
switch (resource) {
case 'userPool':
if (data) {
return {
json: {
UserPool: {
Arn: data.Arn,
CreationDate: data.CreationDate,
DeletionProtection: data.DeletionProtection,
Domain: data.Domain,
EstimatedNumberOfUsers: data.EstimatedNumberOfUsers,
Id: data.Id,
LastModifiedDate: data.LastModifiedDate,
MfaConfiguration: data.MfaConfiguration,
Name: data.Name,
},
},
};
}
break;
case 'user':
if (userData) {
if (operation === 'get') {
const userAttributes = userData.UserAttributes
? mapUserAttributes(userData.UserAttributes)
: {};
return {
json: {
User: {
Enabled: userData.Enabled,
...Object.fromEntries(Object.entries(userAttributes).slice(0, 6)),
UserCreateDate: userData.UserCreateDate,
UserLastModifiedDate: userData.UserLastModifiedDate,
UserStatus: userData.UserStatus,
Username: userData.Username,
},
},
};
} else if (operation === 'getAll') {
if (users && Array.isArray(users)) {
const processedUsers: User[] = [];
users.forEach((user) => {
const userAttributes = user.Attributes ? mapUserAttributes(user.Attributes) : {};
processedUsers.push({
Enabled: userData.Enabled,
...Object.fromEntries(Object.entries(userAttributes).slice(0, 6)),
UserCreateDate: userData.UserCreateDate,
UserLastModifiedDate: userData.UserLastModifiedDate,
UserStatus: userData.UserStatus,
Username: userData.Username,
});
});
return {
json: {
Users: processedUsers,
},
};
}
}
}
break;
}
return undefined;
})
.filter((item) => item !== undefined)
.flat();
return simplifiedItems;
}
export async function listUsersInGroup(
this: IExecuteSingleFunctions,
groupName: string,
userPoolId: string,
): Promise<IDataObject> {
if (!userPoolId) {
throw new ApplicationError('User Pool ID is required');
}
const opts: IHttpRequestOptions = {
url: '',
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.ListUsersInGroup',
},
body: JSON.stringify({
UserPoolId: userPoolId,
GroupName: groupName,
MaxResults: 60,
}),
};
const responseData: IDataObject = await awsRequest.call(this, opts);
const users = responseData.Users as IDataObject[] | undefined;
if (!users) {
return { results: [] };
}
const results: INodeListSearchItems[] = users
.map((user) => {
const attributes = user.Attributes as Array<{ Name: string; Value: string }> | undefined;
const email = attributes?.find((attr) => attr.Name === 'email')?.Value;
const sub = attributes?.find((attr) => attr.Name === 'sub')?.Value;
const username = user.Username as string;
const name = email || sub || username;
const value = username;
return { name, value };
})
.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
return { results, paginationToken: responseData.NextToken as string | undefined };
}
export async function getUsersForGroup(
this: IExecuteSingleFunctions,
groupName: string,
userPoolId: string,
): Promise<IDataObject[]> {
const users = await listUsersInGroup.call(this, groupName, userPoolId);
if (users && users.results && Array.isArray(users.results) && users.results.length > 0) {
return users.results as IDataObject[];
}
return [] as IDataObject[];
}
export async function processUsersForGroups(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const userPoolIdRaw = this.getNodeParameter('userPoolId') as IDataObject;
const userPoolId = userPoolIdRaw.value as string;
const include = this.getNodeParameter('includeUsers', 0) as boolean;
if (!include) {
return items;
}
const processedGroups: IDataObject[] = [];
if (response.body && typeof response.body === 'object') {
if ('Group' in response.body) {
const group = (response.body as { Group: IDataObject }).Group;
const usersResponse = await getUsersForGroup.call(
this,
group.GroupName as string,
userPoolId,
);
if (usersResponse.length > 0) {
return items.map((item) => ({
json: { ...item.json, Users: usersResponse },
}));
} else {
return items.map((item) => ({
json: { ...item.json },
}));
}
} else {
const groups = (response.body as { Groups: IDataObject[] }).Groups;
for (const group of groups) {
const usersResponse = await getUsersForGroup.call(
this,
group.GroupName as string,
userPoolId,
);
if (usersResponse.length > 0) {
processedGroups.push({
...group,
Users: usersResponse,
});
} else {
processedGroups.push(group);
}
}
}
}
return items.map((item) => ({
json: { ...item.json, Groups: processedGroups },
}));
}
//Check if needed
export async function fetchUserPoolConfig(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const userPoolIdRaw = this.getNodeParameter('userPoolId') as IDataObject;
const userPoolId = userPoolIdRaw.value as string;
if (!userPoolId) {
throw new ApplicationError('User Pool ID is required');
}
const opts: IHttpRequestOptions = {
url: '',
method: 'POST',
body: JSON.stringify({
UserPoolId: userPoolId,
}),
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.DescribeUserPool',
},
};
const responseData: IDataObject = await awsRequest.call(this, opts);
return requestOptions;
}

View file

@ -3,8 +3,9 @@ import type { INodeProperties } from 'n8n-workflow';
import {
handleErrorPostReceive,
handlePagination,
presendOptions,
presendPath,
presendAdditionalFields,
presendVerifyPath,
processUsersForGroups,
} from '../GenericFunctions';
export const groupOperations: INodeProperties[] = [
@ -77,7 +78,7 @@ export const groupOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
postReceive: [processUsersForGroups, handleErrorPostReceive],
},
},
action: 'Get group',
@ -101,7 +102,7 @@ export const groupOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
postReceive: [processUsersForGroups, handleErrorPostReceive],
},
},
action: 'Get many groups',
@ -112,7 +113,7 @@ export const groupOperations: INodeProperties[] = [
description: 'Update an existing group',
routing: {
send: {
preSend: [presendOptions],
preSend: [presendAdditionalFields],
},
request: {
method: 'POST',
@ -264,7 +265,7 @@ const createFields: INodeProperties[] = [
send: {
property: 'Path',
type: 'body',
preSend: [presendPath],
preSend: [presendVerifyPath],
},
},
},
@ -492,6 +493,19 @@ const getFields: INodeProperties[] = [
},
],
},
{
displayName: 'Include User List',
name: 'includeUsers',
type: 'boolean',
displayOptions: {
show: {
resource: ['group'],
operation: ['get'],
},
},
default: true,
description: 'Whether to include a list of users in the group',
},
];
const getAllFields: INodeProperties[] = [
@ -553,6 +567,19 @@ const getAllFields: INodeProperties[] = [
displayOptions: { show: { resource: ['group'], operation: ['getAll'], returnAll: [false] } },
routing: { send: { type: 'body', property: 'Limit' } },
},
{
displayName: 'Include User List',
name: 'includeUsers',
type: 'boolean',
displayOptions: {
show: {
resource: ['group'],
operation: ['getAll'],
},
},
default: true,
description: 'Whether to include a list of users in the group',
},
];
const updateFields: INodeProperties[] = [
@ -657,8 +684,8 @@ const updateFields: INodeProperties[] = [
type: 'resourceLocator',
},
{
displayName: 'Options',
name: 'options',
displayName: 'Additional Fields',
name: 'additionalFields',
default: {},
displayOptions: {
show: {
@ -708,7 +735,7 @@ const updateFields: INodeProperties[] = [
send: {
property: 'Path',
type: 'body',
preSend: [presendPath],
preSend: [presendVerifyPath],
},
},
},

View file

@ -1,10 +1,13 @@
import type { INodeProperties } from 'n8n-workflow';
import {
fetchUserPoolConfig,
handleErrorPostReceive,
handlePagination,
presendFilter,
presendTest,
processAttributes,
simplifyData,
} from '../GenericFunctions';
export const userOperations: INodeProperties[] = [
@ -52,6 +55,9 @@ export const userOperations: INodeProperties[] = [
description: 'Create a new user',
action: 'Create user',
routing: {
send: {
preSend: [presendTest, fetchUserPoolConfig],
},
request: {
method: 'POST',
headers: {
@ -104,7 +110,7 @@ export const userOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
postReceive: [simplifyData, handleErrorPostReceive],
},
},
},
@ -130,7 +136,7 @@ export const userOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleErrorPostReceive],
postReceive: [simplifyData, handleErrorPostReceive],
},
},
action: 'Get many users',
@ -167,6 +173,7 @@ export const userOperations: INodeProperties[] = [
description: 'Update a user',
action: 'Update user',
routing: {
send: { preSend: [presendTest] },
request: {
method: 'POST',
headers: {
@ -246,7 +253,7 @@ const createFields: INodeProperties[] = [
displayName: 'User Name',
name: 'Username',
default: '',
description: 'The username of the new user to create',
description: 'The username of the new user to create. No whitespace is allowed.',
placeholder: 'e.g. JohnSmith',
displayOptions: {
show: {
@ -277,61 +284,51 @@ const createFields: INodeProperties[] = [
},
},
options: [
//doesn't work
{
displayName: 'Client Metadata',
name: 'clientMetadata',
displayName: 'User Attributes',
name: 'UserAttributes',
type: 'fixedCollection',
placeholder: 'Add Metadata',
default: { metadata: [] },
description: 'A map of custom key-value pairs for workflows triggered by this action',
placeholder: 'Add Attribute',
default: {
attributes: [],
},
description: 'Attributes to add for the user',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Metadata',
name: 'metadata',
displayName: 'Attributes',
name: 'attributes',
values: [
{
displayName: 'Key',
name: 'key',
displayName: 'Name',
name: 'Name',
type: 'string',
default: '',
description: 'The key of the metadata attribute',
description: 'The name of the attribute (e.g., custom:deliverables)',
},
{
displayName: 'Value',
name: 'value',
name: 'Value',
type: 'string',
default: '',
description: 'The value of the metadata attribute',
description: 'The value of the attribute',
},
],
},
],
routing: {
send: {
preSend: [processAttributes],
type: 'body',
property: 'ClientMetadata',
property: 'UserAttributes',
value:
'={{ $value.metadata && $value.metadata.length > 0 ? Object.fromEntries($value.metadata.map(attribute => [attribute.Name, attribute.Value])) : {} }}',
'={{ $value.attributes?.map(attribute => ({ Name: attribute.Name, Value: attribute.Value })) || [] }}',
},
},
},
{
displayName: 'Temporary Password',
name: 'TemporaryPassword',
default: '',
description: "The user's temporary password",
routing: {
send: {
property: 'TemporaryPassword',
type: 'body',
},
},
type: 'string',
typeOptions: { password: true },
},
{
displayName: 'Message Action',
name: 'MessageAction',
@ -396,88 +393,45 @@ const createFields: INodeProperties[] = [
},
},
{
displayName: 'User Attributes',
name: 'UserAttributes',
type: 'fixedCollection',
placeholder: 'Add Attribute',
default: {
attributes: [],
},
description: 'Attributes to add for the user',
typeOptions: {
multipleValues: true,
},
displayName: 'Temporary Password',
name: 'temporaryPasswordOptions',
type: 'options',
default: 'generatePassword',
description: 'Choose to set a password manually or one will be automatically generated',
options: [
{
displayName: 'Attributes',
name: 'attributes',
values: [
{
displayName: 'Name',
name: 'Name',
type: 'string',
default: '',
description: 'The name of the attribute (e.g., custom:deliverables)',
},
{
displayName: 'Value',
name: 'Value',
type: 'string',
default: '',
description: 'The value of the attribute',
},
],
name: 'Set a Password',
value: 'setPassword',
},
{
name: 'Generate a Password',
value: 'generatePassword',
},
],
routing: {
send: {
preSend: [processAttributes],
property: 'TemporaryPassword',
type: 'body',
property: 'UserAttributes',
value:
'={{ $value.attributes?.map(attribute => ({ Name: attribute.Name, Value: attribute.Value })) || [] }}',
},
},
},
{
displayName: 'Validation Data',
name: 'ValidationData',
type: 'fixedCollection',
placeholder: 'Add Attribute',
default: {
attributes: [],
},
description: 'Validation data to add for the user',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Data',
name: 'data',
values: [
{
displayName: 'Key',
name: 'Key',
type: 'string',
default: '',
description: 'The name of the data (e.g., custom:deliverables)',
},
{
displayName: 'Value',
name: 'Value',
type: 'string',
default: '',
description: 'The value of the data',
},
],
displayName: 'Password',
name: 'password',
type: 'string',
typeOptions: { password: true },
default: '',
placeholder: 'Enter a temporary password',
description: "The user's temporary password",
displayOptions: {
show: {
temporaryPasswordOptions: ['setPassword'],
},
],
},
routing: {
send: {
property: 'TemporaryPassword',
type: 'body',
property: 'ValidationData',
value: '={{ $value.data?.map(data => ({ Name: data.Key, Value: data.Value })) || [] }}',
},
},
},
@ -586,6 +540,19 @@ const getFields: INodeProperties[] = [
required: true,
type: 'resourceLocator',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['get'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];
const getAllFields: INodeProperties[] = [
@ -660,48 +627,35 @@ const getAllFields: INodeProperties[] = [
displayOptions: { show: { resource: ['user'], operation: ['getAll'], returnAll: [false] } },
routing: { send: { type: 'body', property: 'Limit' } },
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: { show: { resource: ['user'], operation: ['getAll'] } },
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Attributes To Get',
name: 'attributesToGet',
type: 'fixedCollection',
typeOptions: { multipleValues: true },
default: {},
placeholder: 'Add Attribute',
description: 'The attributes to return in the response',
options: [
{
name: 'metadataValues',
displayName: 'Metadata',
values: [
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
default: '',
description: 'The attribute name to return',
},
],
},
],
routing: {
send: {
type: 'body',
property: 'AttributesToGet',
value: '={{ $value.metadataValues.map(attribute => attribute.attribute) }}',
},
},
},
{
displayName: 'Filter Attribute',
name: 'filterAttribute',
displayName: 'Filters',
name: 'filters',
type: 'options',
default: 'username',
hint: 'Make sure to select an attribute, type, and provide a value before submitting.',
@ -896,7 +850,7 @@ const updateFields: INodeProperties[] = [
],
},
{
displayName: 'User',
displayName: 'User Name',
name: 'Username',
default: {
mode: 'list',
@ -988,6 +942,7 @@ const updateFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [processAttributes],
type: 'body',
property: 'UserAttributes',
value:
@ -995,62 +950,6 @@ const updateFields: INodeProperties[] = [
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['update'],
},
},
options: [
{
displayName: 'Client Metadata',
name: 'clientMetadata',
type: 'fixedCollection',
placeholder: 'Add Metadata Pair',
default: { metadata: [] },
description: 'A map of custom key-value pairs for workflows triggered by this action',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Metadata',
name: 'metadata',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The key of the metadata attribute',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value of the metadata attribute',
},
],
},
],
routing: {
send: {
type: 'body',
property: 'ClientMetadata',
value:
'={{ $value.metadata && $value.metadata.length > 0 ? Object.fromEntries($value.metadata.map(attribute => [attribute.Name, attribute.Value])) : {} }}',
},
},
},
],
},
];
const addToGroupFields: INodeProperties[] = [

View file

@ -1,4 +1,5 @@
import type { INodeProperties } from 'n8n-workflow';
import { presendTest, simplifyData } from '../GenericFunctions';
export const userPoolOperations: INodeProperties[] = [
{
@ -13,12 +14,26 @@ export const userPoolOperations: INodeProperties[] = [
value: 'get',
action: 'Describe the configuration of a user pool',
routing: {
send: {
preSend: [presendTest],
},
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.DescribeUserPool',
},
},
output: {
postReceive: [
simplifyData,
{
type: 'rootProperty',
properties: {
property: 'UserPool',
},
},
],
},
},
},
],
@ -28,7 +43,7 @@ export const userPoolOperations: INodeProperties[] = [
export const userPoolFields: INodeProperties[] = [
{
displayName: 'User Pool ID',
displayName: 'User Pool',
name: 'userPoolId',
required: true,
type: 'resourceLocator',
@ -69,4 +84,17 @@ export const userPoolFields: INodeProperties[] = [
},
],
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['userPool'],
operation: ['get'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];