From 75dba9a89ac28a2ea60eb52741e1c5346b79b906 Mon Sep 17 00:00:00 2001 From: knowa Date: Wed, 19 Apr 2023 14:15:04 -0400 Subject: [PATCH 01/39] Add support for CrOS Devices in GSuite Admin Node --- .../GSuiteAdminOAuth2Api.credentials.ts | 2 + .../Google/GSuiteAdmin/DeviceDescription.ts | 300 ++++++++++++++++++ .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 107 ++++++- .../Google/GSuiteAdmin/GenericFunctions.ts | 1 + 4 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts diff --git a/packages/nodes-base/credentials/GSuiteAdminOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GSuiteAdminOAuth2Api.credentials.ts index 4a7720de0c832..523227220ac34 100644 --- a/packages/nodes-base/credentials/GSuiteAdminOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GSuiteAdminOAuth2Api.credentials.ts @@ -5,6 +5,8 @@ const scopes = [ 'https://www.googleapis.com/auth/admin.directory.user', 'https://www.googleapis.com/auth/admin.directory.domain.readonly', 'https://www.googleapis.com/auth/admin.directory.userschema.readonly', + 'https://www.googleapis.com/auth/admin.directory.device.chromeos', + 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly', ]; export class GSuiteAdminOAuth2Api implements ICredentialType { diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts new file mode 100644 index 0000000000000..e647a4b16a3c0 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -0,0 +1,300 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const deviceOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['device'], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a device', + action: 'Get a device', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many devices', + action: 'Get many devices', + }, + { + name: 'Update', + value: 'update', + description: 'Update a device', + action: 'Update a device', + }, + { + name: 'Change Status', + value: 'changeStatus', + description: 'Change the Status of a Chromebook', + action: 'Set the status of a device', + }, + ], + default: 'get', + }, +]; + +export const deviceFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* device:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'UUID', + name: 'uuid', + type: 'string', + required: true, + displayOptions: { + show: { + operation: ['get', 'update', 'changeStatus'], + resource: ['device'], + }, + }, + default: '', + }, + /* -------------------------------------------------------------------------- */ + /* device:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All?', + name: 'returnAll', + type: 'boolean', + default: false, + // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['device'], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['device'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'Projection', + name: 'projection', + type: 'options', + required: true, + options: [ + { + name: 'Basic', + value: 'basic', + description: 'Do not include any custom fields for the user', + }, + { + name: 'Full', + value: 'full', + description: 'Include all fields associated with this user', + }, + ], + displayOptions: { + show: { + operation: ['get', 'getAll', 'update'], + resource: ['device'], + }, + }, + default: 'basic', + description: 'What subset of fields to fetch for this device', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['device'], + }, + }, + options: [ + { + displayName: 'Organizational Unit Name or ID', + name: 'orgUnitPath', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getOrgUnits', + }, + default: [], + description: + 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Include Children?', + name: 'includeChildOrgunits', + type: 'boolean', + default: false, + // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether + description: + 'Include devices from organizational units below your specified organizational unit', + }, + { + displayName: 'Order By', + name: 'orderBy', + type: 'options', + options: [ + { + name: 'Annotated Location', + value: 'annotatedLocation', + }, + { + name: 'Annotated User', + value: 'annotatedUser', + }, + { + name: 'Last Sync', + value: 'lastSync', + }, + { + name: 'Notes', + value: 'notes', + }, + { + name: 'Serial Number', + value: 'serialNumber', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Support End Date', + value: 'supportEndDate', + }, + ], + default: '', + description: 'Property to use for sorting results', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ascending', + }, + { + name: 'Descending', + value: 'descending', + }, + ], + default: '', + description: 'Property to use for sorting results. Must accompany Order By variable.', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: "Must use Google's querying syntax", + }, + ], + }, + { + displayName: 'Update Fields', + name: 'updateOptions', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['update'], + resource: ['device'], + }, + }, + options: [ + { + displayName: 'Move to Organizational Unit Name or ID', + name: 'orgUnitPath', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getOrgUnits', + }, + default: [], + description: + 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Annotated User', + name: 'annotatedUser', + type: 'string', + default: '', + description: 'The annotated User of the device', + }, + { + displayName: 'Annotated Location', + name: 'annotatedLocation', + type: 'string', + default: '', + description: 'The annotated Location of the device', + }, + { + displayName: 'Annotated Asset ID', + name: 'annotatedAssetId', + type: 'string', + default: '', + description: 'The annotated Asset ID of a device', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Add notes to a device', + }, + ], + }, + { + displayName: 'Action', + name: 'action', + type: 'options', + required: true, + options: [ + { + name: 'Enable', + value: 'reenable', + description: 'Re-enable a disabled chromebook', + action: 'Enable a device', + }, + { + name: 'Disable', + value: 'disable', + description: 'Disable a chromebook', + action: 'Disable a device', + }, + ], + displayOptions: { + show: { + operation: ['changeStatus'], + resource: ['device'], + }, + }, + default: 'Enable', + description: 'Set the status of a device', + }, +]; diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 1d7a435748711..8ec38e58795f4 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -13,6 +13,8 @@ import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; import { userFields, userOperations } from './UserDescription'; +import { deviceFields, deviceOperations } from './DeviceDescription'; + import { groupFields, groupOperations } from './GroupDescripion'; export class GSuiteAdmin implements INodeType { @@ -51,13 +53,19 @@ export class GSuiteAdmin implements INodeType { name: 'User', value: 'user', }, + { + name: 'Device', + value: 'device', + }, ], - default: 'user', + default: 'device', }, ...groupOperations, ...groupFields, ...userOperations, ...userFields, + ...deviceOperations, + ...deviceFields, ], }; @@ -103,6 +111,23 @@ export class GSuiteAdmin implements INodeType { } return returnData; }, + async getOrgUnits(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const orgUnits = await googleApiRequest.call( + this, + 'GET', + '/directory/v1/customer/my_customer/orgunits?orgUnitPath=/&type=all', + ); + for (const orgUnit of orgUnits.organizationUnits) { + const orgUnitName = orgUnit.orgUnitPath; + returnData.push({ + name: orgUnitName, + value: orgUnitName, + }); + } + console.log(returnData); + return returnData; + }, }, }; @@ -416,6 +441,86 @@ export class GSuiteAdmin implements INodeType { } } + if (resource === 'device') { + //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/uuid + if (operation === 'get') { + const uuid = this.getNodeParameter('uuid', i) as string; + const projection = this.getNodeParameter('projection', 1); + responseData = await googleApiRequest.call( + this, + 'GET', + `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, + {}, + ); + } + + //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/ + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + const projection = this.getNodeParameter('projection', 1); + + const options = this.getNodeParameter('options', 2); + + qs.projection = projection; + Object.assign(qs, options); + + if (qs.customer === undefined) { + qs.customer = 'my_customer'; + } + + console.log(qs); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'chromeosdevices', + 'GET', + `/directory/v1/customer/${qs.customer}/devices/chromeos/`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i); + + responseData = await googleApiRequest.call( + this, + 'GET', + `/directory/v1/customer/${qs.customer}/devices/chromeos/`, + {}, + qs, + ); + } + } + + if (operation === 'update') { + const uuid = this.getNodeParameter('uuid', i) as string; + const projection = this.getNodeParameter('projection', 1); + const updateOptions = this.getNodeParameter('updateOptions', 1); + + Object.assign(qs, updateOptions); + responseData = await googleApiRequest.call( + this, + 'PUT', + `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, + qs, + ); + } + + if (operation === 'changeStatus') { + const uuid = this.getNodeParameter('uuid', i) as string; + const action = this.getNodeParameter('action', 1); + + qs.action = action; + console.log(qs); + responseData = await googleApiRequest.call( + this, + 'POST', + `/directory/v1/customer/my_customer/devices/chromeos/${uuid}/action`, + qs, + ); + } + } + const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray(responseData as IDataObject[]), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index 848bab2e96935..dfed4a5c10129 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -59,6 +59,7 @@ export async function googleApiRequestAllItems( do { responseData = await googleApiRequest.call(this, method, endpoint, body, query); + console.log(responseData); query.pageToken = responseData.nextPageToken; returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); } while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== ''); From df3fd33da0c6d8cf3dcf9ed20dff02bd1cf67821 Mon Sep 17 00:00:00 2001 From: knowa Date: Wed, 19 Apr 2023 14:24:34 -0400 Subject: [PATCH 02/39] Removed unnecessary logging strings --- .../nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 2 +- .../nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 8ec38e58795f4..3bc9f3ee60767 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -58,7 +58,7 @@ export class GSuiteAdmin implements INodeType { value: 'device', }, ], - default: 'device', + default: 'user', }, ...groupOperations, ...groupFields, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index dfed4a5c10129..848bab2e96935 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -59,7 +59,6 @@ export async function googleApiRequestAllItems( do { responseData = await googleApiRequest.call(this, method, endpoint, body, query); - console.log(responseData); query.pageToken = responseData.nextPageToken; returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); } while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== ''); From 053a4f954c09af26b3db9dae2eab388f201766a4 Mon Sep 17 00:00:00 2001 From: knowa Date: Wed, 19 Apr 2023 14:27:03 -0400 Subject: [PATCH 03/39] Removed unnecessary print statements --- .../nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 3bc9f3ee60767..4e39d2d5fe502 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -125,7 +125,6 @@ export class GSuiteAdmin implements INodeType { value: orgUnitName, }); } - console.log(returnData); return returnData; }, }, @@ -468,8 +467,6 @@ export class GSuiteAdmin implements INodeType { qs.customer = 'my_customer'; } - console.log(qs); - if (returnAll) { responseData = await googleApiRequestAllItems.call( this, @@ -511,7 +508,6 @@ export class GSuiteAdmin implements INodeType { const action = this.getNodeParameter('action', 1); qs.action = action; - console.log(qs); responseData = await googleApiRequest.call( this, 'POST', From 3c17cbb18a3ff733222e88c7da954836e4bb3ea6 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Wed, 18 Dec 2024 20:51:00 +0200 Subject: [PATCH 04/39] update group logic --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 224 +++++++++++------ .../Google/GSuiteAdmin/GenericFunctions.ts | 42 ++++ .../Google/GSuiteAdmin/GroupDescripion.ts | 237 +++++++++++++----- 3 files changed, 356 insertions(+), 147 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index f49b00fbd04c9..998292be2e65f 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -7,15 +7,12 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; - -import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; - -import { userFields, userOperations } from './UserDescription'; +import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { deviceFields, deviceOperations } from './DeviceDescription'; - +import { googleApiRequest, googleApiRequestAllItems, searchGroups } from './GenericFunctions'; import { groupFields, groupOperations } from './GroupDescripion'; +import { userFields, userOperations } from './UserDescription'; export class GSuiteAdmin implements INodeType { description: INodeTypeDescription = { @@ -128,6 +125,9 @@ export class GSuiteAdmin implements INodeType { return returnData; }, }, + listSearch: { + searchGroups, + }, }; async execute(this: IExecuteFunctions): Promise { @@ -143,11 +143,13 @@ export class GSuiteAdmin implements INodeType { if (resource === 'group') { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/insert if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; const email = this.getNodeParameter('email', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i); const body: IDataObject = { + name, email, }; @@ -158,8 +160,17 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/delete if (operation === 'delete') { - const groupId = this.getNodeParameter('groupId', i) as string; + const groupIdRaw = this.getNodeParameter('groupId', i) as any; + // Extract the 'value' field if the resourceLocator object is returned + const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + + if (!groupId) { + throw new NodeOperationError( + this.getNode(), + 'Group ID is required but was not provided.', + ); + } responseData = await googleApiRequest.call( this, 'DELETE', @@ -172,8 +183,18 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/get if (operation === 'get') { - const groupId = this.getNodeParameter('groupId', i) as string; + // Retrieve the selected group ID from the resource locator + const groupIdRaw = this.getNodeParameter('groupId', i) as any; + + // Extract the 'value' field if the resourceLocator object is returned + const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + if (!groupId) { + throw new NodeOperationError( + this.getNode(), + 'Group ID is required but was not provided.', + ); + } responseData = await googleApiRequest.call( this, 'GET', @@ -181,19 +202,47 @@ export class GSuiteAdmin implements INodeType { {}, ); } - //https://developers.google.com/admin-sdk/directory/v1/reference/groups/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i); - const options = this.getNodeParameter('options', i); + // Retrieve filter parameters + const filter = this.getNodeParameter('filter', i, {}) as IDataObject; + const sort = this.getNodeParameter('sort', i, {}) as IDataObject; - Object.assign(qs, options); + //TODO Check how t osend correct query field + if (typeof filter.query === 'string') { + const query = filter.query.trim(); - if (qs.customer === undefined) { + // Validate the query format + const regex = /^(name|email):\S+$/; + if (!regex.test(query)) { + throw new ApplicationError( + 'Invalid query format. Query must follow the format "displayName:" or "email:".', + ); + } + + qs.query = query; + } + + // Handle sort options + if (sort.sortRules) { + const { orderBy, sortOrder } = sort.sortRules as { + orderBy?: string; + sortOrder?: string; + }; + if (orderBy) { + qs.orderBy = orderBy; + } + if (sortOrder) { + qs.sortOrder = sortOrder; + } + } + if (!qs.customer) { qs.customer = 'my_customer'; } + // Fetch all or limited results if (returnAll) { responseData = await googleApiRequestAllItems.call( this, @@ -214,13 +263,24 @@ export class GSuiteAdmin implements INodeType { qs, ); - responseData = responseData.groups; + responseData = responseData.groups || []; } } //https://developers.google.com/admin-sdk/directory/v1/reference/groups/update if (operation === 'update') { - const groupId = this.getNodeParameter('groupId', i) as string; + // Retrieve the selected group ID from the resource locator + const groupIdRaw = this.getNodeParameter('groupId', i) as any; + + // Extract the 'value' field if the resourceLocator object is returned + const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + + if (!groupId) { + throw new NodeOperationError( + this.getNode(), + 'Group ID is required but was not provided.', + ); + } const updateFields = this.getNodeParameter('updateFields', i); @@ -449,93 +509,93 @@ export class GSuiteAdmin implements INodeType { delete body.emailUi; } - responseData = await googleApiRequest.call( - this, - 'PUT', - `/directory/v1/users/${userId}`, - body, - qs, - ); + responseData = await googleApiRequest.call( + this, + 'PUT', + `/directory/v1/users/${userId}`, + body, + qs, + ); + } } - } - if (resource === 'device') { - //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/uuid - if (operation === 'get') { - const uuid = this.getNodeParameter('uuid', i) as string; - const projection = this.getNodeParameter('projection', 1); - responseData = await googleApiRequest.call( - this, - 'GET', - `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, - {}, - ); - } + if (resource === 'device') { + //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/uuid + if (operation === 'get') { + const uuid = this.getNodeParameter('uuid', i) as string; + const projection = this.getNodeParameter('projection', 1); + responseData = await googleApiRequest.call( + this, + 'GET', + `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, + {}, + ); + } + + //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/ + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + const projection = this.getNodeParameter('projection', 1); - //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/ - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - const projection = this.getNodeParameter('projection', 1); + const options = this.getNodeParameter('options', 2); - const options = this.getNodeParameter('options', 2); + qs.projection = projection; + Object.assign(qs, options); - qs.projection = projection; - Object.assign(qs, options); + if (qs.customer === undefined) { + qs.customer = 'my_customer'; + } - if (qs.customer === undefined) { - qs.customer = 'my_customer'; + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'chromeosdevices', + 'GET', + `/directory/v1/customer/${qs.customer}/devices/chromeos/`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i); + + responseData = await googleApiRequest.call( + this, + 'GET', + `/directory/v1/customer/${qs.customer}/devices/chromeos/`, + {}, + qs, + ); + } } - if (returnAll) { - responseData = await googleApiRequestAllItems.call( + if (operation === 'update') { + const uuid = this.getNodeParameter('uuid', i) as string; + const projection = this.getNodeParameter('projection', 1); + const updateOptions = this.getNodeParameter('updateOptions', 1); + + Object.assign(qs, updateOptions); + responseData = await googleApiRequest.call( this, - 'chromeosdevices', - 'GET', - `/directory/v1/customer/${qs.customer}/devices/chromeos/`, - {}, + 'PUT', + `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, qs, ); - } else { - qs.maxResults = this.getNodeParameter('limit', i); + } + + if (operation === 'changeStatus') { + const uuid = this.getNodeParameter('uuid', i) as string; + const action = this.getNodeParameter('action', 1); + qs.action = action; responseData = await googleApiRequest.call( this, - 'GET', - `/directory/v1/customer/${qs.customer}/devices/chromeos/`, - {}, + 'POST', + `/directory/v1/customer/my_customer/devices/chromeos/${uuid}/action`, qs, ); } } - if (operation === 'update') { - const uuid = this.getNodeParameter('uuid', i) as string; - const projection = this.getNodeParameter('projection', 1); - const updateOptions = this.getNodeParameter('updateOptions', 1); - - Object.assign(qs, updateOptions); - responseData = await googleApiRequest.call( - this, - 'PUT', - `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, - qs, - ); - } - - if (operation === 'changeStatus') { - const uuid = this.getNodeParameter('uuid', i) as string; - const action = this.getNodeParameter('action', 1); - - qs.action = action; - responseData = await googleApiRequest.call( - this, - 'POST', - `/directory/v1/customer/my_customer/devices/chromeos/${uuid}/action`, - qs, - ); - } - } - const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray(responseData as IDataObject[]), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index 7f30fe6812e29..fd867fa2cef7f 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -5,6 +5,8 @@ import type { JsonObject, IHttpRequestMethods, IRequestOptions, + INodeListSearchResult, + INodeListSearchItems, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; @@ -64,3 +66,43 @@ export async function googleApiRequestAllItems( return returnData; } + +export async function searchGroups( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const qs: IDataObject = { + customer: 'my_customer', + }; + + // Add filtering if a filter is provided + if (filter) { + qs.query = `name:${filter}* OR email:${filter}*`; + } + + // Perform the API request to list all groups + const responseData = await googleApiRequestAllItems.call( + this, + 'groups', + 'GET', + '/directory/v1/groups', + {}, + qs, // Query string + ); + + // Handle cases where no groups are found + if (!responseData || responseData.length === 0) { + console.warn('No groups found in the response'); + return { results: [] }; + } + + // Map the API response to the desired format + const results: INodeListSearchItems[] = responseData.map( + (group: { name?: string; email?: string; id?: string }) => ({ + name: group.name || group.email || 'Unnamed Group', + value: group.id || group.email, + }), + ); + + return { results }; +} diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts index 3646800f9abc8..0cdfa90682d1e 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts @@ -51,11 +51,26 @@ export const groupFields: INodeProperties[] = [ /* -------------------------------------------------------------------------- */ /* group:create */ /* -------------------------------------------------------------------------- */ + + { + displayName: 'Group Name', + name: 'name', + displayOptions: { + show: { + operation: ['create'], + resource: ['group'], + }, + }, + default: '', + description: "The group's display name", + placeholder: 'e.g. Sales', + type: 'string', + }, { - displayName: 'Email', + displayName: 'Group Email', name: 'email', type: 'string', - placeholder: 'name@email.com', + placeholder: 'e.g. sales@example.com', required: true, displayOptions: { show: { @@ -88,40 +103,62 @@ export const groupFields: INodeProperties[] = [ description: 'An extended description to help users determine the purpose of a group. For example, you can include information about who should join the group, the types of messages to send to the group, links to FAQs about the group, or related groups.', }, - { - displayName: 'Name', - name: 'name', - type: 'string', - default: '', - description: "The group's display name", - }, ], }, /* -------------------------------------------------------------------------- */ /* group:delete */ /* -------------------------------------------------------------------------- */ { - displayName: 'Group ID', + displayName: 'Group', name: 'groupId', - type: 'string', - required: true, + default: { + mode: 'list', + value: '', + }, + description: 'Select the group you want to delete', displayOptions: { show: { operation: ['delete'], resource: ['group'], }, }, - default: '', - description: - "Identifies the group in the API request. The value can be the group's email address, group alias, or the unique group ID.", + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'GroupId', + type: 'string', + hint: 'Enter the group name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The group name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. Admins', + }, + ], + required: true, + type: 'resourceLocator', }, /* -------------------------------------------------------------------------- */ /* group:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Group ID', + displayName: 'Group', name: 'groupId', - type: 'string', + type: 'resourceLocator', required: true, displayOptions: { show: { @@ -129,13 +166,34 @@ export const groupFields: INodeProperties[] = [ resource: ['group'], }, }, - default: '', - description: - "Identifies the group in the API request. The value can be the group's email address, group alias, or the unique group ID.", + default: { + mode: 'list', + value: '', + }, + description: 'Select the group you want to retrieve', + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'groupId', + type: 'string', + hint: 'Enter the group id', + placeholder: 'e.g. 0123kx3o1habcdf', + }, + ], }, /* -------------------------------------------------------------------------- */ /* group:getAll */ /* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -168,10 +226,10 @@ export const groupFields: INodeProperties[] = [ description: 'Max number of results to return', }, { - displayName: 'Options', - name: 'options', + displayName: 'Filter', + name: 'filter', type: 'collection', - placeholder: 'Add option', + placeholder: 'Add Filter', default: {}, displayOptions: { show: { @@ -185,61 +243,80 @@ export const groupFields: INodeProperties[] = [ name: 'customer', type: 'string', default: '', - description: - "The unique ID for the customer's Google Workspace account. In case of a multi-domain account, to fetch all groups for a customer, fill this field instead of domain.", + description: "The unique ID for the customer's Google Workspace account", }, { displayName: 'Domain', name: 'domain', type: 'string', default: '', - description: 'The domain name. Use this field to get fields from only one domain.', + description: 'The domain name. Use this field to get groups from a specific domain.', }, { - displayName: 'Order By', - name: 'orderBy', - type: 'options', - options: [ - { - name: 'Email', - value: 'email', - }, - ], + displayName: 'Query', + name: 'query', + type: 'string', + placeholder: 'e.g. name:Admins', default: '', - description: 'Property to use for sorting results', + description: 'Query string to filter the results. Follow Google Admin SDK documentation.', }, { - displayName: 'Query', - name: 'query', + displayName: 'User ID', + name: 'userId', type: 'string', default: '', - description: - 'Query string search. Complete documentation is at.', + description: 'Email or immutable ID of a user to list groups they are a member of', }, + ], + }, + { + displayName: 'Sort', + name: 'sort', + type: 'fixedCollection', + placeholder: 'Add Sort Rule', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['group'], + }, + }, + options: [ { - displayName: 'Sort Order', - name: 'sortOrder', - type: 'options', - options: [ + name: 'sortRules', + displayName: 'Sort Rules', + values: [ { - name: 'Ascending', - value: 'ASCENDING', + displayName: 'Order By', + name: 'orderBy', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + ], + default: '', + description: 'Field to sort the results by', }, { - name: 'Descending', - value: 'DESCENDING', + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASCENDING', + }, + { + name: 'Descending', + value: 'DESCENDING', + }, + ], + default: 'ASCENDING', + description: 'Sort order direction', }, ], - default: '', - description: 'Whether to return results in ascending or descending order', - }, - { - displayName: 'User ID', - name: 'userId', - type: 'string', - default: '', - description: - "Email or immutable ID of the user if only those groups are to be listed, the given user is a member of. If it's an ID, it should match with the ID of the user object.", }, ], }, @@ -247,19 +324,48 @@ export const groupFields: INodeProperties[] = [ /* group:update */ /* -------------------------------------------------------------------------- */ { - displayName: 'Group ID', + displayName: 'Group', name: 'groupId', - type: 'string', - required: true, displayOptions: { show: { operation: ['update'], resource: ['group'], }, }, - default: '', - description: - "Identifies the group in the API request. The value can be the group's email address, group alias, or the unique group ID.", + default: { + mode: 'list', + value: '', + }, + description: 'Select the group you want to update', + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'groupId', + type: 'string', + hint: 'Enter the group name', + validation: [ + { + type: 'regex', + properties: { + regex: '^[\\w+=,.@-]+$', + errorMessage: 'The group name must follow the allowed pattern.', + }, + }, + ], + placeholder: 'e.g. Admins', + }, + ], + required: true, + type: 'resourceLocator', }, { displayName: 'Update Fields', @@ -286,7 +392,7 @@ export const groupFields: INodeProperties[] = [ displayName: 'Email', name: 'email', type: 'string', - placeholder: 'name@email.com', + placeholder: 'e.g. sales@example.com', default: '', description: "The group's email address. If your account has multiple domains, select the appropriate domain for the email address. The email must be unique.", @@ -295,6 +401,7 @@ export const groupFields: INodeProperties[] = [ displayName: 'Name', name: 'name', type: 'string', + placeholder: 'e.g. Sales', default: '', description: "The group's display name", }, From 9ee822505c8f3dff26e2ffbcaa0d037692b72d4c Mon Sep 17 00:00:00 2001 From: Stamsy Date: Thu, 19 Dec 2024 11:46:11 +0200 Subject: [PATCH 05/39] update resource locator for group description --- .../Google/GSuiteAdmin/GroupDescripion.ts | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts index 0cdfa90682d1e..2aceb545fc885 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts @@ -136,17 +136,8 @@ export const groupFields: INodeProperties[] = [ displayName: 'By ID', name: 'GroupId', type: 'string', - hint: 'Enter the group name', - validation: [ - { - type: 'regex', - properties: { - regex: '^[\\w+=,.@-]+$', - errorMessage: 'The group name must follow the allowed pattern.', - }, - }, - ], - placeholder: 'e.g. Admins', + hint: 'Enter the group id', + placeholder: 'e.g. 0123kx3o1habcdf', }, ], required: true, @@ -351,17 +342,9 @@ export const groupFields: INodeProperties[] = [ displayName: 'By ID', name: 'groupId', type: 'string', - hint: 'Enter the group name', - validation: [ - { - type: 'regex', - properties: { - regex: '^[\\w+=,.@-]+$', - errorMessage: 'The group name must follow the allowed pattern.', - }, - }, - ], - placeholder: 'e.g. Admins', + hint: 'Enter the group id', + + placeholder: 'e.g. 0123kx3o1habcdf', }, ], required: true, From 4dde15e160c443277546f0af837dea62003b31c2 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Sat, 21 Dec 2024 01:08:44 +0200 Subject: [PATCH 06/39] format description for device description --- .../nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index e647a4b16a3c0..05aaf4ef936de 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -141,7 +141,7 @@ export const deviceFields: INodeProperties[] = [ }, default: [], description: - 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression.', + 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression.', }, { displayName: 'Include Children?', @@ -237,7 +237,7 @@ export const deviceFields: INodeProperties[] = [ }, default: [], description: - 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression.', + 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression.', }, { displayName: 'Annotated User', From d481b26a8896f611e2264c73f748f38af4057db3 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Sat, 21 Dec 2024 01:09:55 +0200 Subject: [PATCH 07/39] add helper functions for list users and groups --- .../Google/GSuiteAdmin/GenericFunctions.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index fd867fa2cef7f..20417d0b2e01d 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -67,6 +67,47 @@ export async function googleApiRequestAllItems( return returnData; } +/* listSearch methods */ +export async function searchUsers( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const qs: IDataObject = { + customer: 'my_customer', + }; + + // Apply a filter if provided + if (filter) { + qs.query = `name:${filter}* OR email:${filter}*`; + } + + // Perform the API request to list all users + const responseData = await googleApiRequestAllItems.call( + this, + 'users', + 'GET', + '/directory/v1/users', + {}, + qs, + ); + + // Handle cases where no users are found + if (!responseData || responseData.length === 0) { + console.warn('No users found in the response'); + return { results: [] }; + } + + // Map the API response to the desired format + const results: INodeListSearchItems[] = responseData.map( + (user: { name?: { fullName?: string }; primaryEmail?: string; id?: string }) => ({ + name: user.name?.fullName || user.primaryEmail || 'Unnamed User', + value: user.id || user.primaryEmail, + }), + ); + + return { results }; +} + export async function searchGroups( this: ILoadOptionsFunctions, filter?: string, From cc35478ce52c420f4a426725e768a6c1ae95bd59 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Sat, 21 Dec 2024 01:16:59 +0200 Subject: [PATCH 08/39] update user description logic and structure --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 339 +++++-- .../Google/GSuiteAdmin/UserDescription.ts | 900 ++++++++++++++---- 2 files changed, 979 insertions(+), 260 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 998292be2e65f..a17c0e5a244c9 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -10,7 +10,12 @@ import type { import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { deviceFields, deviceOperations } from './DeviceDescription'; -import { googleApiRequest, googleApiRequestAllItems, searchGroups } from './GenericFunctions'; +import { + googleApiRequest, + googleApiRequestAllItems, + searchGroups, + searchUsers, +} from './GenericFunctions'; import { groupFields, groupOperations } from './GroupDescripion'; import { userFields, userOperations } from './UserDescription'; @@ -127,6 +132,7 @@ export class GSuiteAdmin implements INodeType { }, listSearch: { searchGroups, + searchUsers, }, }; @@ -145,7 +151,6 @@ export class GSuiteAdmin implements INodeType { if (operation === 'create') { const name = this.getNodeParameter('name', i) as string; const email = this.getNodeParameter('email', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i); const body: IDataObject = { @@ -161,14 +166,13 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/delete if (operation === 'delete') { const groupIdRaw = this.getNodeParameter('groupId', i) as any; - - // Extract the 'value' field if the resourceLocator object is returned const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; if (!groupId) { throw new NodeOperationError( this.getNode(), 'Group ID is required but was not provided.', + { itemIndex: i }, ); } responseData = await googleApiRequest.call( @@ -183,16 +187,14 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/get if (operation === 'get') { - // Retrieve the selected group ID from the resource locator const groupIdRaw = this.getNodeParameter('groupId', i) as any; - - // Extract the 'value' field if the resourceLocator object is returned const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; if (!groupId) { throw new NodeOperationError( this.getNode(), 'Group ID is required but was not provided.', + { itemIndex: i }, ); } responseData = await googleApiRequest.call( @@ -205,12 +207,10 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i); - - // Retrieve filter parameters const filter = this.getNodeParameter('filter', i, {}) as IDataObject; const sort = this.getNodeParameter('sort', i, {}) as IDataObject; - //TODO Check how t osend correct query field + //TODO Check how to send correct query field if (typeof filter.query === 'string') { const query = filter.query.trim(); @@ -269,16 +269,14 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/update if (operation === 'update') { - // Retrieve the selected group ID from the resource locator const groupIdRaw = this.getNodeParameter('groupId', i) as any; - - // Extract the 'value' field if the resourceLocator object is returned const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; if (!groupId) { throw new NodeOperationError( this.getNode(), 'Group ID is required but was not provided.', + { itemIndex: i }, ); } @@ -298,20 +296,58 @@ export class GSuiteAdmin implements INodeType { } if (resource === 'user') { + //https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/insert + if (operation === 'addToGroup') { + const groupIdRaw = this.getNodeParameter('groupId', i) as any; + const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + + const userIdRaw = this.getNodeParameter('userId', i) as any; + const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; + + let userEmail: string | undefined; + + // If the user ID is not already an email, fetch the user details + if (!userId.includes('@')) { + console.log('User ID is not an email; fetching user details...'); + const userDetails = await googleApiRequest.call( + this, + 'GET', + `/directory/v1/users/${userId}`, + ); + userEmail = userDetails.primaryEmail; + } else { + userEmail = userId; + } + + if (!userEmail) { + throw new ApplicationError( + 'Unable to determine the user email for adding to the group.', + ); + } + + const body: IDataObject = { + email: userEmail, + role: 'MEMBER', + }; + + responseData = await googleApiRequest.call( + this, + 'POST', + `/directory/v1/groups/${groupId}/members`, + body, + ); + + responseData = { added: true }; + } + //https://developers.google.com/admin-sdk/directory/v1/reference/users/insert if (operation === 'create') { const domain = this.getNodeParameter('domain', i) as string; - const firstName = this.getNodeParameter('firstName', i) as string; - const lastName = this.getNodeParameter('lastName', i) as string; - const password = this.getNodeParameter('password', i) as string; - const username = this.getNodeParameter('username', i) as string; - const makeAdmin = this.getNodeParameter('makeAdmin', i) as boolean; - const additionalFields = this.getNodeParameter('additionalFields', i); const body: IDataObject = { @@ -327,20 +363,34 @@ export class GSuiteAdmin implements INodeType { if (additionalFields.phoneUi) { const phones = (additionalFields.phoneUi as IDataObject).phoneValues as IDataObject[]; - body.phones = phones; - delete body.phoneUi; } if (additionalFields.emailUi) { const emails = (additionalFields.emailUi as IDataObject).emailValues as IDataObject[]; - body.emails = emails; - delete body.emailUi; } + if (additionalFields.roles) { + const roles = (additionalFields.roles as IDataObject).rolesValues as IDataObject; + + body.roles = { + superAdmin: Boolean(roles.superAdmin), + groupsAdmin: Boolean(roles.groupsAdmin), + groupsReader: Boolean(roles.groupsReader), + groupsEditor: Boolean(roles.groupsEditor), + userManagement: Boolean(roles.userManagement), + helpDeskAdmin: Boolean(roles.helpDeskAdmin), + servicesAdmin: Boolean(roles.servicesAdmin), + inventoryReportingAdmin: Boolean(roles.inventoryReportingAdmin), + storageAdmin: Boolean(roles.storageAdmin), + directorySyncAdmin: Boolean(roles.directorySyncAdmin), + mobileAdmin: Boolean(roles.mobileAdmin), + }; + } + responseData = await googleApiRequest.call( this, 'POST', @@ -348,22 +398,20 @@ export class GSuiteAdmin implements INodeType { body, qs, ); - - if (makeAdmin) { - await googleApiRequest.call( - this, - 'POST', - `/directory/v1/users/${responseData.id}/makeAdmin`, - { status: true }, - ); - - responseData.isAdmin = true; - } } //https://developers.google.com/admin-sdk/directory/v1/reference/users/delete if (operation === 'delete') { - const userId = this.getNodeParameter('userId', i) as string; + const userIdRaw = this.getNodeParameter('userId', i) as any; + const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; + + if (!userId) { + throw new NodeOperationError( + this.getNode(), + 'User ID is required but was not provided.', + { itemIndex: i }, + ); + } responseData = await googleApiRequest.call( this, @@ -372,33 +420,38 @@ export class GSuiteAdmin implements INodeType { {}, ); - responseData = { success: true }; + responseData = { deleted: true }; } //https://developers.google.com/admin-sdk/directory/v1/reference/users/get if (operation === 'get') { - const userId = this.getNodeParameter('userId', i) as string; + const userIdRaw = this.getNodeParameter('userId', i) as any; + const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; - const projection = this.getNodeParameter('projection', i) as string; + const output = this.getNodeParameter('output', i); + const projection = this.getNodeParameter('projection', i); + const fields = this.getNodeParameter('fields', i, []) as string[]; - const options = this.getNodeParameter('options', i); - - qs.projection = projection; - - Object.assign(qs, options); - - if (qs.customFieldMask) { - qs.customFieldMask = (qs.customFieldMask as string[]).join(' '); - } - - if (qs.projection === 'custom' && qs.customFieldMask === undefined) { + // Validate User ID + if (!userId) { throw new NodeOperationError( this.getNode(), - 'When projection is set to custom, the custom schemas field must be defined', + 'User ID is required but was not provided.', { itemIndex: i }, ); } + if (projection) { + qs.projection = projection; + } + + if (output === 'select') { + if (!fields.includes('id')) { + fields.push('id'); + } + qs.fields = fields.join(','); + } + responseData = await googleApiRequest.call( this, 'GET', @@ -406,35 +459,72 @@ export class GSuiteAdmin implements INodeType { {}, qs, ); + + if (output === 'simplified') { + responseData = { + kind: responseData.kind, + id: responseData.id, + primaryEmail: responseData.primaryEmail, + name: responseData.name, + isAdmin: responseData.isAdmin, + lastLoginTime: responseData.lastLoginTime, + creationTime: responseData.creationTime, + suspended: responseData.suspended, + }; + } } //https://developers.google.com/admin-sdk/directory/v1/reference/users/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i); - + const output = this.getNodeParameter('output', i); + const fields = this.getNodeParameter('fields', i, []) as string[]; const projection = this.getNodeParameter('projection', i) as string; + const filter = this.getNodeParameter('filter', i, {}) as IDataObject; + const sort = this.getNodeParameter('sort', i, {}) as IDataObject; - const options = this.getNodeParameter('options', i); - - qs.projection = projection; - - Object.assign(qs, options); - - if (qs.customer === undefined) { - qs.customer = 'my_customer'; + if (typeof filter.query === 'string') { + const query = filter.query.trim(); + if (query) { + qs.query = query; + } } - if (qs.customFieldMask) { - qs.customFieldMask = (qs.customFieldMask as string[]).join(' '); + if (sort.sortRules) { + const { orderBy, sortOrder } = sort.sortRules as { + orderBy?: string; + sortOrder?: string; + }; + if (orderBy) { + qs.orderBy = orderBy; + } + if (sortOrder) { + qs.sortOrder = sortOrder; + } } - if (qs.projection === 'custom' && qs.customFieldMask === undefined) { + qs.projection = projection; + if (projection === 'custom' && !qs.customFieldMask) { throw new NodeOperationError( this.getNode(), 'When projection is set to custom, the custom schemas field must be defined', { itemIndex: i }, ); } + if (qs.customFieldMask) { + qs.customFieldMask = (qs.customFieldMask as string[]).join(','); + } + + if (output === 'select') { + if (!fields.includes('id')) { + fields.push('id'); + } + qs.fields = `users(${fields.join(',')})`; + } + + if (!qs.customer) { + qs.customer = 'my_customer'; + } if (returnAll) { responseData = await googleApiRequestAllItems.call( @@ -447,7 +537,6 @@ export class GSuiteAdmin implements INodeType { ); } else { qs.maxResults = this.getNodeParameter('limit', i); - responseData = await googleApiRequest.call( this, 'GET', @@ -458,55 +547,104 @@ export class GSuiteAdmin implements INodeType { responseData = responseData.users; } + + if (output === 'simplified') { + responseData = responseData.map((user: any) => ({ + kind: user.kind, + id: user.id, + primaryEmail: user.primaryEmail, + name: user.name, + isAdmin: user.isAdmin, + lastLoginTime: user.lastLoginTime, + creationTime: user.creationTime, + suspended: user.suspended, + })); + } + } + + //https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/delete + if (operation === 'removeFromGroup') { + const groupIdRaw = this.getNodeParameter('groupId', i) as any; + const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + const userIdRaw = this.getNodeParameter('userId', i) as any; + const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; + const body: IDataObject = { + email: userId, + role: 'MEMBER', + }; + + await googleApiRequest.call( + this, + 'DELETE', + `/directory/v1/groups/${groupId}/members/${userId}`, + ); + + responseData = { removed: true }; } //https://developers.google.com/admin-sdk/directory/v1/reference/users/update if (operation === 'update') { - const userId = this.getNodeParameter('userId', i) as string; - + const userIdRaw = this.getNodeParameter('userId', i) as any; + const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; const updateFields = this.getNodeParameter('updateFields', i); + // Validate User ID + if (!userId) { + throw new NodeOperationError( + this.getNode(), + 'User ID is required but was not provided.', + { itemIndex: i }, + ); + } + const body: { - name: { givenName?: string; familyName?: string }; + name?: { givenName?: string; familyName?: string }; emails?: IDataObject[]; phones?: IDataObject[]; - } = { name: {} }; - - Object.assign(body, updateFields); + suspended?: boolean; + roles?: { [key: string]: boolean }; + } = {}; if (updateFields.firstName) { + body.name = body.name || {}; body.name.givenName = updateFields.firstName as string; - //@ts-ignore - delete body.firstName; } if (updateFields.lastName) { + body.name = body.name || {}; body.name.familyName = updateFields.lastName as string; - //@ts-ignore - delete body.lastName; - } - - if (Object.keys(body.name).length === 0) { - //@ts-ignore - delete body.name; } if (updateFields.phoneUi) { const phones = (updateFields.phoneUi as IDataObject).phoneValues as IDataObject[]; - body.phones = phones; - - //@ts-ignore - delete body.phoneUi; } if (updateFields.emailUi) { const emails = (updateFields.emailUi as IDataObject).emailValues as IDataObject[]; - body.emails = emails; + } - //@ts-ignore - delete body.emailUi; + if (typeof updateFields.suspendUi === 'boolean') { + body.suspended = updateFields.suspendUi; // Map directly to suspended + } + + if (updateFields.roles) { + const roles = (updateFields.roles as IDataObject).rolesValues as IDataObject; + + body.roles = { + superAdmin: Boolean(roles.superAdmin), + groupsAdmin: Boolean(roles.groupsAdmin), + groupsReader: Boolean(roles.groupsReader), + groupsEditor: Boolean(roles.groupsEditor), + userManagement: Boolean(roles.userManagement), + helpDeskAdmin: Boolean(roles.helpDeskAdmin), + servicesAdmin: Boolean(roles.servicesAdmin), + inventoryReportingAdmin: Boolean(roles.inventoryReportingAdmin), + storageAdmin: Boolean(roles.storageAdmin), + directorySyncAdmin: Boolean(roles.directorySyncAdmin), + mobileAdmin: Boolean(roles.mobileAdmin), + }; } responseData = await googleApiRequest.call( @@ -524,6 +662,15 @@ export class GSuiteAdmin implements INodeType { if (operation === 'get') { const uuid = this.getNodeParameter('uuid', i) as string; const projection = this.getNodeParameter('projection', 1); + + // Validate uuid + if (!uuid) { + throw new NodeOperationError( + this.getNode(), + 'uuid is required but was not provided.', + { itemIndex: i }, + ); + } responseData = await googleApiRequest.call( this, 'GET', @@ -573,6 +720,14 @@ export class GSuiteAdmin implements INodeType { const projection = this.getNodeParameter('projection', 1); const updateOptions = this.getNodeParameter('updateOptions', 1); + // Validate uuid + if (!uuid) { + throw new NodeOperationError( + this.getNode(), + 'uuid is required but was not provided.', + { itemIndex: i }, + ); + } Object.assign(qs, updateOptions); responseData = await googleApiRequest.call( this, @@ -605,13 +760,23 @@ export class GSuiteAdmin implements INodeType { } catch (error) { if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), + this.helpers.returnJsonArray({ + message: `Operation "${operation}" failed for resource "${resource}".`, + description: error.message, + }), { itemData: { item: i } }, ); returnData.push(...executionErrorData); continue; } - throw error; + throw new NodeOperationError( + this.getNode(), + `Operation "${operation}" failed for resource "${resource}".`, + { + description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.message}`, + itemIndex: i, + }, + ); } } return [returnData]; diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index f4dccd8fe72d7..1cf577fc3199c 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -12,6 +12,12 @@ export const userOperations: INodeProperties[] = [ }, }, options: [ + { + name: 'Add to Group', + value: 'addToGroup', + description: 'Add an existing user to a group', + action: 'Add user to group', + }, { name: 'Create', value: 'create', @@ -36,6 +42,12 @@ export const userOperations: INodeProperties[] = [ description: 'Get many users', action: 'Get many users', }, + { + name: 'Remove From Group', + value: 'removeFromGroup', + description: 'Remove a user from a group', + action: 'Remove user from group', + }, { name: 'Update', value: 'update', @@ -48,12 +60,93 @@ export const userOperations: INodeProperties[] = [ ]; export const userFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* user:addToGroup */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User', + name: 'userId', + required: true, + type: 'resourceLocator', + default: { + mode: 'list', + value: '', + }, + description: 'Select the user you want to add to the group', + displayOptions: { + show: { + resource: ['user'], + operation: ['addToGroup'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchUsers', + searchable: true, + }, + }, + { + displayName: 'By Email', + name: 'userEmail', + type: 'string', + hint: 'Enter the user email', + placeholder: 'e.g. sales@example.com', + }, + { + displayName: 'By ID', + name: 'userId', + type: 'string', + hint: 'Enter the user id', + placeholder: 'e.g. 123456789879230471055', + }, + ], + }, + { + displayName: 'Group', + name: 'groupId', + required: true, + type: 'resourceLocator', + default: { + mode: 'list', + value: '', + }, + description: 'Select the group you want to add the user to', + displayOptions: { + show: { + resource: ['user'], + operation: ['addToGroup'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'groupId', + type: 'string', + hint: 'Enter the group id', + placeholder: 'e.g. 0123kx3o1habcdf', + }, + ], + }, /* -------------------------------------------------------------------------- */ /* user:create */ /* -------------------------------------------------------------------------- */ { displayName: 'First Name', name: 'firstName', + placeholder: 'e.g. Nathan', type: 'string', required: true, displayOptions: { @@ -69,6 +162,7 @@ export const userFields: INodeProperties[] = [ name: 'lastName', type: 'string', required: true, + placeholder: 'e.g. Smith', displayOptions: { show: { operation: ['create'], @@ -95,28 +189,11 @@ export const userFields: INodeProperties[] = [ description: 'Stores the password for the user account. A minimum of 8 characters is required. The maximum length is 100 characters.', }, - { - displayName: 'Domain Name or ID', - name: 'domain', - type: 'options', - description: - 'Choose from the list, or specify an ID using an expression', - typeOptions: { - loadOptionsMethod: 'getDomains', - }, - required: true, - displayOptions: { - show: { - operation: ['create'], - resource: ['user'], - }, - }, - default: '', - }, { displayName: 'Username', name: 'username', type: 'string', + placeholder: 'e.g. n.smith', displayOptions: { show: { operation: ['create'], @@ -125,12 +202,17 @@ export const userFields: INodeProperties[] = [ }, default: '', description: - "The username that will be set to the user. Example: If you domain is example.com and you set the username to jhon then the user's final email address will be jhon@example.com.", + "The username that will be set to the user. Example: If you domain is example.com and you set the username to n.smith then the user's final email address will be n.smith@example.com.", }, { - displayName: 'Make Admin', - name: 'makeAdmin', - type: 'boolean', + displayName: 'Domain Name or ID', + name: 'domain', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getDomains', + }, required: true, displayOptions: { show: { @@ -138,8 +220,7 @@ export const userFields: INodeProperties[] = [ resource: ['user'], }, }, - default: false, - description: 'Whether to make a user a super administrator', + default: '', }, { displayName: 'Additional Fields', @@ -155,9 +236,8 @@ export const userFields: INodeProperties[] = [ }, options: [ { - displayName: 'Change Password At Next Login', + displayName: 'Change Password at Next Login', name: 'changePasswordAtNextLogin', - type: 'boolean', default: false, description: 'Whether the user is forced to change their password at next login', @@ -282,8 +362,7 @@ export const userFields: INodeProperties[] = [ name: 'primary', type: 'boolean', default: false, - description: - "Whether this is the user's primary phone number. A user may only have one primary phone number.", + description: "Whether this is the user's primary phone number", }, ], }, @@ -334,6 +413,135 @@ export const userFields: INodeProperties[] = [ }, ], }, + { + displayName: 'Roles', + name: 'roles', + type: 'fixedCollection', + placeholder: 'Assign Roles', + typeOptions: { + multipleValues: false, + }, + default: {}, + description: 'Select the roles you want to assign to the user', + options: [ + { + name: 'rolesValues', + displayName: 'Roles', + values: [ + { + displayName: 'Super Admin', + name: 'superAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Super Admin role', + }, + { + displayName: 'Groups Admin', + name: 'groupsAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Groups Admin role', + }, + { + displayName: 'Groups Reader', + name: 'groupsReader', + type: 'boolean', + default: false, + description: 'Whether Assign Groups Reader role', + }, + { + displayName: 'Groups Editor', + name: 'groupsEditor', + type: 'boolean', + default: false, + description: 'Whether Assign Groups Editor role', + }, + { + displayName: 'User Management', + name: 'userManagement', + type: 'boolean', + default: false, + description: 'Whether Assign User Management role', + }, + { + displayName: 'Help Desk Admin', + name: 'helpDeskAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Help Desk Admin role', + }, + { + displayName: 'Services Admin', + name: 'servicesAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Services Admin role', + }, + { + displayName: 'Inventory Reporting Admin', + name: 'inventoryReportingAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Inventory Reporting Admin role', + }, + { + displayName: 'Storage Admin', + name: 'storageAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Storage Admin role', + }, + { + displayName: 'Directory Sync Admin', + name: 'directorySyncAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Directory Sync Admin role', + }, + { + displayName: 'Mobile Admin', + name: 'mobileAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Mobile Admin role', + }, + ], + }, + ], + }, + { + displayName: 'Custom Fields', + name: 'customFields', + placeholder: 'Add or Edit Custom Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + description: 'Allows editing and adding of custom fields', + options: [ + { + name: 'fieldValues', + displayName: 'Field', + values: [ + { + displayName: 'Field Name', + name: 'fieldName', + type: 'string', + default: '', + description: 'The name of the custom field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value for the custom field', + }, + ], + }, + ], + }, ], }, /* -------------------------------------------------------------------------- */ @@ -342,17 +550,44 @@ export const userFields: INodeProperties[] = [ { displayName: 'User ID', name: 'userId', - type: 'string', - required: true, + default: { + mode: 'list', + value: '', + }, displayOptions: { show: { operation: ['delete'], resource: ['user'], }, }, - default: '', - description: - "The value can be the user's primary email address, alias email address, or unique user ID", + description: 'Select the user you want to delete', + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchUsers', + searchable: true, + }, + }, + { + displayName: 'By Email', + name: 'userEmail', + type: 'string', + hint: 'Enter the user email', + placeholder: 'e.g. sales@example.com', + }, + { + displayName: 'By ID', + name: 'userId', + type: 'string', + hint: 'Enter the user id', + placeholder: 'e.g. 123456789879230471055', + }, + ], + required: true, + type: 'resourceLocator', }, /* -------------------------------------------------------------------------- */ /* user:get */ @@ -360,7 +595,7 @@ export const userFields: INodeProperties[] = [ { displayName: 'User ID', name: 'userId', - type: 'string', + type: 'resourceLocator', required: true, displayOptions: { show: { @@ -368,9 +603,90 @@ export const userFields: INodeProperties[] = [ resource: ['user'], }, }, - default: '', - description: - "The value can be the user's primary email address, alias email address, or unique user ID", + default: { + mode: 'list', + value: '', + }, + description: 'Select the user you want to retrieve', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchUsers', + searchable: true, + }, + }, + { + displayName: 'By Email', + name: 'userEmail', + type: 'string', + hint: 'Enter the user email', + placeholder: 'e.g. sales@example.com', + }, + { + displayName: 'By ID', + name: 'groupId', + type: 'string', + hint: 'Enter the user id', + placeholder: 'e.g. 0123kx3o1habcdf', + }, + ], + }, + { + displayName: 'Output', + name: 'output', + type: 'options', + required: true, + default: 'simplified', + displayOptions: { + show: { + operation: ['get'], + resource: ['user'], + }, + }, + options: [ + { + name: 'Simplified', + value: 'simplified', + description: + 'Only return specific fields: kind, ID, primaryEmail, name (with subfields), isAdmin, lastLoginTime, creationTime, and suspended', + }, + { + name: 'Raw', + value: 'raw', + description: 'Return all fields from the API response', + }, + { + name: 'Select Included Fields', + value: 'select', + description: 'Choose specific fields to include', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + default: [], + displayOptions: { + show: { + output: ['select'], + operation: ['get'], + resource: ['user'], + }, + }, + options: [ + { name: 'creationTime', value: 'creationTime' }, + { name: 'isAdmin', value: 'isAdmin' }, + { name: 'Kind', value: 'kind' }, + { name: 'lastLoginTime', value: 'lastLoginTime' }, + { name: 'Name', value: 'name' }, + { name: 'primaryEmail', value: 'primaryEmail' }, + { name: 'Suspended', value: 'suspended' }, + ], + description: 'Fields to include in the response when "Select Included Fields" is chosen', }, { displayName: 'Projection', @@ -379,82 +695,47 @@ export const userFields: INodeProperties[] = [ required: true, options: [ { - name: 'Basic', + name: 'Don’t Include', value: 'basic', description: 'Do not include any custom fields for the user', }, { name: 'Custom', value: 'custom', - description: 'Include custom fields from schemas requested in customField', + description: 'Include custom fields from schemas requested in Custom Schema Names or IDs', }, { - name: 'Full', + name: 'Include All', value: 'full', description: 'Include all fields associated with this user', }, ], + default: 'basic', displayOptions: { show: { operation: ['get'], resource: ['user'], }, }, - default: 'basic', description: 'What subset of fields to fetch for this user', }, { - displayName: 'Options', - name: 'options', - type: 'collection', - placeholder: 'Add option', - default: {}, + displayName: 'Custom Schema Names or IDs', + name: 'customFieldMask', + type: 'multiOptions', displayOptions: { show: { operation: ['get'], resource: ['user'], + '/projection': ['custom'], }, }, - options: [ - { - displayName: 'Custom Schema Names or IDs', - name: 'customFieldMask', - type: 'multiOptions', - displayOptions: { - show: { - '/projection': ['custom'], - }, - }, - typeOptions: { - loadOptionsMethod: 'getSchemas', - }, - default: [], - description: - 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression.', - }, - { - displayName: 'View Type', - name: 'viewType', - type: 'options', - options: [ - { - name: 'Admin View', - value: 'admin_view', - description: - 'Results include both administrator-only and domain-public fields for the user', - }, - { - name: 'Descending', - value: 'DESCENDING', - description: - 'Results only include fields for the user that are publicly visible to other users in the domain', - }, - ], - default: 'admin_view', - description: - 'Whether to fetch the administrator-only or domain-wide public view of the user. For more information, see Retrieve a user as a non-administrator.', - }, - ], + typeOptions: { + loadOptionsMethod: 'getSchemas', + }, + default: [], + description: + 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression.', }, /* -------------------------------------------------------------------------- */ /* user:getAll */ @@ -490,42 +771,114 @@ export const userFields: INodeProperties[] = [ default: 100, description: 'Max number of results to return', }, + { + displayName: 'Output', + name: 'output', + type: 'options', + required: true, + default: 'simplified', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['user'], + }, + }, + options: [ + { + name: 'Simplified', + value: 'simplified', + description: + 'Only return specific fields: kind, ID, primaryEmail, name (with subfields), isAdmin, lastLoginTime, creationTime, and suspended', + }, + { + name: 'Raw', + value: 'raw', + description: 'Return all fields from the API response', + }, + { + name: 'Select Included Fields', + value: 'select', + description: 'Choose specific fields to include', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + default: [], + displayOptions: { + show: { + output: ['select'], + operation: ['getAll'], + resource: ['user'], + }, + }, + options: [ + { name: 'creationTime', value: 'creationTime' }, + { name: 'isAdmin', value: 'isAdmin' }, + { name: 'Kind', value: 'kind' }, + { name: 'lastLoginTime', value: 'lastLoginTime' }, + { name: 'Name', value: 'name' }, + { name: 'primaryEmail', value: 'primaryEmail' }, + { name: 'Suspended', value: 'suspended' }, + ], + description: 'Fields to include in the response when "Select Included Fields" is chosen', + }, { displayName: 'Projection', name: 'projection', type: 'options', required: true, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['user'], + }, + }, options: [ { - name: 'Basic', + name: 'Don’t Include', value: 'basic', description: 'Do not include any custom fields for the user', }, { name: 'Custom', value: 'custom', - description: 'Include custom fields from schemas requested in customField', + description: 'Include custom fields from schemas requested in Custom Schema Names or IDs', }, { - name: 'Full', + name: 'Include All', value: 'full', description: 'Include all fields associated with this user', }, ], + default: 'basic', + description: 'What subset of fields to fetch for this user', + }, + { + displayName: 'Custom Schema Names or IDs', + name: 'customFieldMask', + type: 'multiOptions', displayOptions: { show: { operation: ['getAll'], resource: ['user'], + '/projection': ['custom'], }, }, - default: 'basic', - description: 'What subset of fields to fetch for this user', + typeOptions: { + loadOptionsMethod: 'getSchemas', + }, + default: [], + description: + 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression.', }, { - displayName: 'Options', - name: 'options', + displayName: 'Filter', + name: 'filter', type: 'collection', - placeholder: 'Add option', + placeholder: 'Add Filter', default: {}, displayOptions: { show: { @@ -534,111 +887,176 @@ export const userFields: INodeProperties[] = [ }, }, options: [ - { - displayName: 'Custom Schema Names or IDs', - name: 'customFieldMask', - type: 'multiOptions', - displayOptions: { - show: { - '/projection': ['custom'], - }, - }, - typeOptions: { - loadOptionsMethod: 'getSchemas', - }, - default: [], - description: - 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression.', - }, { displayName: 'Customer', name: 'customer', type: 'string', default: '', - description: - "The unique ID for the customer's Google Workspace account. In case of a multi-domain account, to fetch all groups for a customer, fill this field instead of domain.", + description: "The unique ID for the customer's Google Workspace account", }, { displayName: 'Domain', name: 'domain', type: 'string', default: '', - description: 'The domain name. Use this field to get fields from only one domain.', - }, - { - displayName: 'Order By', - name: 'orderBy', - type: 'options', - options: [ - { - name: 'Email', - value: 'email', - }, - { - name: 'Family Name', - value: 'familyName', - }, - { - name: 'Given Name', - value: 'givenName', - }, - ], - default: '', - description: 'Property to use for sorting results', + description: 'The domain name. Use this field to get groups from a specific domain.', }, { displayName: 'Query', name: 'query', type: 'string', + placeholder: 'e.g. name:Admins', default: '', - description: - 'Free text search terms to find users that match these terms in any field, except for extended properties. For more information on constructing user queries, see Search for Users.', + description: 'Query string to filter the results. Follow Google Admin SDK documentation.', }, { displayName: 'Show Deleted', name: 'showDeleted', type: 'boolean', default: false, - description: 'Whether to retrieve the list of deleted users', + description: 'Whether retrieve the list of deleted users', + }, + ], + }, + { + displayName: 'Sort', + name: 'sort', + type: 'fixedCollection', + placeholder: 'Add Sort Rule', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['user'], }, + }, + options: [ { - displayName: 'Sort Order', - name: 'sortOrder', - type: 'options', - options: [ + name: 'sortRules', + displayName: 'Sort Rules', + values: [ { - name: 'Ascending', - value: 'ASCENDING', + displayName: 'Order By', + name: 'orderBy', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Family Name', + value: 'familyName', + }, + { + name: 'Given Name', + value: 'givenName', + }, + ], + default: '', + description: 'Field to sort the results by', }, { - name: 'Descending', - value: 'DESCENDING', + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASCENDING', + }, + { + name: 'Descending', + value: 'DESCENDING', + }, + ], + default: 'ASCENDING', + description: 'Sort order direction', }, ], - default: '', - description: 'Whether to return results in ascending or descending order', }, + ], + description: 'Define sorting rules for the results', + }, + + /* -------------------------------------------------------------------------- */ + /* user:removeFromGroup */ + /* -------------------------------------------------------------------------- */ + + { + displayName: 'User', + name: 'userId', + required: true, + type: 'resourceLocator', + default: { + mode: 'list', + value: '', + }, + description: 'Select the user you want to remove from the group', + displayOptions: { + show: { + resource: ['user'], + operation: ['removeFromGroup'], + }, + }, + modes: [ { - displayName: 'View Type', - name: 'viewType', - type: 'options', - options: [ - { - name: 'Admin View', - value: 'admin_view', - description: - 'Results include both administrator-only and domain-public fields for the user', - }, - { - name: 'Descending', - value: 'DESCENDING', - description: - 'Results only include fields for the user that are publicly visible to other users in the domain', - }, - ], - default: 'admin_view', - description: - 'Whether to fetch the administrator-only or domain-wide public view of the user. For more information, see Retrieve a user as a non-administrator.', + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchUsers', + searchable: true, + }, + }, + { + displayName: 'By Email', + name: 'userEmail', + type: 'string', + hint: 'Enter the user email', + placeholder: 'e.g. sales@example.com', + }, + { + displayName: 'By ID', + name: 'userId', + type: 'string', + hint: 'Enter the user id', + placeholder: 'e.g. 123456789879230471055', + }, + ], + }, + { + displayName: 'Group', + name: 'groupId', + required: true, + type: 'resourceLocator', + default: { + mode: 'list', + value: '', + }, + description: 'Select the group you want to remove the user from', + displayOptions: { + show: { + resource: ['user'], + operation: ['removeFromGroup'], + }, + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'groupId', + type: 'string', + hint: 'Enter the group id', + placeholder: 'e.g. 0123kx3o1habcdf', }, ], }, @@ -648,7 +1066,7 @@ export const userFields: INodeProperties[] = [ { displayName: 'User ID', name: 'userId', - type: 'string', + type: 'resourceLocator', required: true, displayOptions: { show: { @@ -656,9 +1074,36 @@ export const userFields: INodeProperties[] = [ resource: ['user'], }, }, - default: '', - description: - "The value can be the user's primary email address, alias email address, or unique user ID", + default: { + mode: 'list', + value: '', + }, + modes: [ + { + displayName: 'From list', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchUsers', + searchable: true, + }, + }, + { + displayName: 'By Email', + name: 'userEmail', + type: 'string', + hint: 'Enter the user email', + placeholder: 'e.g. sales@example.com', + }, + { + displayName: 'By ID', + name: 'userId', + type: 'string', + hint: 'Enter the user id', + placeholder: 'e.g. 123456789879230471055', + }, + ], + description: 'Select the user you want to update', }, { displayName: 'Update Fields', @@ -681,9 +1126,16 @@ export const userFields: INodeProperties[] = [ description: 'Whether user is archived', }, { - displayName: 'Change Password At Next Login', + displayName: 'Suspend', + name: 'suspendUi', + type: 'boolean', + default: false, + description: + 'Whether to set the user as suspended. If set to OFF, the user will be reactivated. If not added, the status will remain unchanged.', + }, + { + displayName: 'Change Password at Next Login', name: 'changePasswordAtNextLogin', - type: 'boolean', default: false, description: 'Whether the user is forced to change their password at next login', @@ -693,12 +1145,14 @@ export const userFields: INodeProperties[] = [ name: 'firstName', type: 'string', default: '', + placeholder: 'e.g. John', }, { displayName: 'Last Name', name: 'lastName', type: 'string', default: '', + placeholder: 'e.g. Doe', }, { displayName: 'Password', @@ -706,6 +1160,7 @@ export const userFields: INodeProperties[] = [ type: 'string', typeOptions: { password: true }, default: '', + placeholder: 'e.g. MyStrongP@ssword123', description: 'Stores the password for the user account. A minimum of 8 characters is required. The maximum length is 100 characters.', }, @@ -821,6 +1276,7 @@ export const userFields: INodeProperties[] = [ name: 'value', type: 'string', default: '', + placeholder: 'e.g. +1234567890', }, { displayName: 'Primary', @@ -839,6 +1295,7 @@ export const userFields: INodeProperties[] = [ name: 'primaryEmail', type: 'string', default: '', + placeholder: 'e.g. john.doe@example.com', description: "The user's primary email address. This property is required in a request to create a user account. The primaryEmail must be unique and cannot be an alias of another user.", }, @@ -882,6 +1339,103 @@ export const userFields: INodeProperties[] = [ name: 'address', type: 'string', default: '', + placeholder: 'e.g. john.doe.work@example.com', + }, + ], + }, + ], + }, + { + displayName: 'Roles', + name: 'roles', + type: 'fixedCollection', + placeholder: 'Assign Roles', + typeOptions: { + multipleValues: false, + }, + default: {}, + description: 'Select the roles you want to assign to the user', + options: [ + { + name: 'rolesValues', + displayName: 'Roles', + values: [ + { + displayName: 'Super Admin', + name: 'superAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Super Admin role', + }, + { + displayName: 'Groups Admin', + name: 'groupsAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Groups Admin role', + }, + { + displayName: 'Groups Reader', + name: 'groupsReader', + type: 'boolean', + default: false, + description: 'Whether Assign Groups Reader role', + }, + { + displayName: 'Groups Editor', + name: 'groupsEditor', + type: 'boolean', + default: false, + description: 'Whether Assign Groups Editor role', + }, + { + displayName: 'User Management', + name: 'userManagement', + type: 'boolean', + default: false, + description: 'Whether Assign User Management role', + }, + { + displayName: 'Help Desk Admin', + name: 'helpDeskAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Help Desk Admin role', + }, + { + displayName: 'Services Admin', + name: 'servicesAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Services Admin role', + }, + { + displayName: 'Inventory Reporting Admin', + name: 'inventoryReportingAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Inventory Reporting Admin role', + }, + { + displayName: 'Storage Admin', + name: 'storageAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Storage Admin role', + }, + { + displayName: 'Directory Sync Admin', + name: 'directorySyncAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Directory Sync Admin role', + }, + { + displayName: 'Mobile Admin', + name: 'mobileAdmin', + type: 'boolean', + default: false, + description: 'Whether Assign Mobile Admin role', }, ], }, From 631cd2c4b1aadb7ad5efbff690fa4fbcab830346 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Tue, 31 Dec 2024 00:09:36 +0200 Subject: [PATCH 09/39] fix update primary email --- .../nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index a17c0e5a244c9..e9885725983b0 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -600,6 +600,7 @@ export class GSuiteAdmin implements INodeType { const body: { name?: { givenName?: string; familyName?: string }; emails?: IDataObject[]; + primaryEmail?: string; phones?: IDataObject[]; suspended?: boolean; roles?: { [key: string]: boolean }; @@ -625,8 +626,12 @@ export class GSuiteAdmin implements INodeType { body.emails = emails; } + if (updateFields.primaryEmail) { + body.primaryEmail = updateFields.primaryEmail as string; + } + if (typeof updateFields.suspendUi === 'boolean') { - body.suspended = updateFields.suspendUi; // Map directly to suspended + body.suspended = updateFields.suspendUi; } if (updateFields.roles) { From 169137967c811a1547b87221d5cbad0ff30c31b6 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Mon, 6 Jan 2025 15:37:56 +0200 Subject: [PATCH 10/39] fix comments and add regex validation for email --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 8 +--- .../Google/GSuiteAdmin/GenericFunctions.ts | 8 ++-- .../Google/GSuiteAdmin/UserDescription.ts | 45 +++++++++++++++++++ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index e9885725983b0..c50c8677afd12 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -73,8 +73,7 @@ export class GSuiteAdmin implements INodeType { methods = { loadOptions: { - // Get all the domains to display them to user so that they can - // select them easily + // Get all the domains async getDomains(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const domains = await googleApiRequestAllItems.call( @@ -93,8 +92,7 @@ export class GSuiteAdmin implements INodeType { } return returnData; }, - // Get all the schemas to display them to user so that they can - // select them easily + //Get all the schemas async getSchemas(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const schemas = await googleApiRequestAllItems.call( @@ -209,8 +207,6 @@ export class GSuiteAdmin implements INodeType { const returnAll = this.getNodeParameter('returnAll', i); const filter = this.getNodeParameter('filter', i, {}) as IDataObject; const sort = this.getNodeParameter('sort', i, {}) as IDataObject; - - //TODO Check how to send correct query field if (typeof filter.query === 'string') { const query = filter.query.trim(); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index 20417d0b2e01d..ad988b558ec33 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -76,7 +76,6 @@ export async function searchUsers( customer: 'my_customer', }; - // Apply a filter if provided if (filter) { qs.query = `name:${filter}* OR email:${filter}*`; } @@ -97,7 +96,7 @@ export async function searchUsers( return { results: [] }; } - // Map the API response to the desired format + //Map the API response const results: INodeListSearchItems[] = responseData.map( (user: { name?: { fullName?: string }; primaryEmail?: string; id?: string }) => ({ name: user.name?.fullName || user.primaryEmail || 'Unnamed User', @@ -116,7 +115,6 @@ export async function searchGroups( customer: 'my_customer', }; - // Add filtering if a filter is provided if (filter) { qs.query = `name:${filter}* OR email:${filter}*`; } @@ -128,7 +126,7 @@ export async function searchGroups( 'GET', '/directory/v1/groups', {}, - qs, // Query string + qs, ); // Handle cases where no groups are found @@ -137,7 +135,7 @@ export async function searchGroups( return { results: [] }; } - // Map the API response to the desired format + //Map the API response const results: INodeListSearchItems[] = responseData.map( (group: { name?: string; email?: string; id?: string }) => ({ name: group.name || group.email || 'Unnamed Group', diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index 1cf577fc3199c..d4853a42928b1 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -95,6 +95,15 @@ export const userFields: INodeProperties[] = [ type: 'string', hint: 'Enter the user email', placeholder: 'e.g. sales@example.com', + validation: [ + { + type: 'regex', + properties: { + regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + errorMessage: 'Please enter a valid email address.', + }, + }, + ], }, { displayName: 'By ID', @@ -577,6 +586,15 @@ export const userFields: INodeProperties[] = [ type: 'string', hint: 'Enter the user email', placeholder: 'e.g. sales@example.com', + validation: [ + { + type: 'regex', + properties: { + regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + errorMessage: 'Please enter a valid email address.', + }, + }, + ], }, { displayName: 'By ID', @@ -624,6 +642,15 @@ export const userFields: INodeProperties[] = [ type: 'string', hint: 'Enter the user email', placeholder: 'e.g. sales@example.com', + validation: [ + { + type: 'regex', + properties: { + regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + errorMessage: 'Please enter a valid email address.', + }, + }, + ], }, { displayName: 'By ID', @@ -1015,6 +1042,15 @@ export const userFields: INodeProperties[] = [ type: 'string', hint: 'Enter the user email', placeholder: 'e.g. sales@example.com', + validation: [ + { + type: 'regex', + properties: { + regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + errorMessage: 'Please enter a valid email address.', + }, + }, + ], }, { displayName: 'By ID', @@ -1094,6 +1130,15 @@ export const userFields: INodeProperties[] = [ type: 'string', hint: 'Enter the user email', placeholder: 'e.g. sales@example.com', + validation: [ + { + type: 'regex', + properties: { + regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + errorMessage: 'Please enter a valid email address.', + }, + }, + ], }, { displayName: 'By ID', From b628d9edae81cde4a52f205d914c71e8093aa62d Mon Sep 17 00:00:00 2001 From: Stamsy Date: Mon, 6 Jan 2025 20:19:14 +0200 Subject: [PATCH 11/39] add placeholders for Device resource --- .../nodes/Google/GSuiteAdmin/DeviceDescription.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index 05aaf4ef936de..afc4abed6b80e 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -57,6 +57,7 @@ export const deviceFields: INodeProperties[] = [ }, }, default: '', + placeholder: 'e.g. 123e4567-e89b-12d3-a456-426614174000', }, /* -------------------------------------------------------------------------- */ /* device:getAll */ @@ -210,6 +211,7 @@ export const deviceFields: INodeProperties[] = [ displayName: 'Query', name: 'query', type: 'string', + placeholder: 'e.g. name:contact* email:contact*', default: '', description: "Must use Google's querying syntax", }, @@ -245,6 +247,7 @@ export const deviceFields: INodeProperties[] = [ type: 'string', default: '', description: 'The annotated User of the device', + placeholder: 'e.g. help desk', }, { displayName: 'Annotated Location', @@ -252,6 +255,7 @@ export const deviceFields: INodeProperties[] = [ type: 'string', default: '', description: 'The annotated Location of the device', + placeholder: 'e.g. Mountain View help desk Chromebook', }, { displayName: 'Annotated Asset ID', @@ -259,6 +263,7 @@ export const deviceFields: INodeProperties[] = [ type: 'string', default: '', description: 'The annotated Asset ID of a device', + placeholder: 'e.g. 1234567890', }, { displayName: 'Notes', @@ -266,6 +271,7 @@ export const deviceFields: INodeProperties[] = [ type: 'string', default: '', description: 'Add notes to a device', + placeholder: 'e.g. Loaned from support', }, ], }, From dce7291d1e8c5fae99a51e115bd2f50ef1a7abad Mon Sep 17 00:00:00 2001 From: Stamsy Date: Tue, 7 Jan 2025 08:31:40 +0200 Subject: [PATCH 12/39] fix resource locator --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 31 ++++++++++++------- .../Google/GSuiteAdmin/GenericFunctions.ts | 18 ++--------- .../Google/GSuiteAdmin/GroupDescripion.ts | 8 ++--- .../Google/GSuiteAdmin/UserDescription.ts | 14 +++------ 4 files changed, 30 insertions(+), 41 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index c50c8677afd12..874b31321faff 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -9,7 +9,6 @@ import type { } from 'n8n-workflow'; import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; -import { deviceFields, deviceOperations } from './DeviceDescription'; import { googleApiRequest, googleApiRequestAllItems, @@ -18,6 +17,7 @@ import { } from './GenericFunctions'; import { groupFields, groupOperations } from './GroupDescripion'; import { userFields, userOperations } from './UserDescription'; +import { deviceFields, deviceOperations } from './DeviceDescription'; export class GSuiteAdmin implements INodeType { description: INodeTypeDescription = { @@ -440,7 +440,10 @@ export class GSuiteAdmin implements INodeType { if (projection) { qs.projection = projection; } - + qs.projection = projection; + if (projection === 'custom' && qs.customFieldMask) { + qs.customFieldMask = (qs.customFieldMask as string[]).join(','); + } if (output === 'select') { if (!fields.includes('id')) { fields.push('id'); @@ -479,13 +482,25 @@ export class GSuiteAdmin implements INodeType { const filter = this.getNodeParameter('filter', i, {}) as IDataObject; const sort = this.getNodeParameter('sort', i, {}) as IDataObject; - if (typeof filter.query === 'string') { + if (filter.customer) { + qs.customer = filter.customer as string; + } + + if (filter.domain) { + qs.domain = filter.domain as string; + } + + if (filter.query && typeof filter.query === 'string') { const query = filter.query.trim(); if (query) { qs.query = query; } } + if (filter.showDeleted) { + qs.showDeleted = filter.showDeleted === true ? 'true' : 'false'; + } + if (sort.sortRules) { const { orderBy, sortOrder } = sort.sortRules as { orderBy?: string; @@ -500,14 +515,7 @@ export class GSuiteAdmin implements INodeType { } qs.projection = projection; - if (projection === 'custom' && !qs.customFieldMask) { - throw new NodeOperationError( - this.getNode(), - 'When projection is set to custom, the custom schemas field must be defined', - { itemIndex: i }, - ); - } - if (qs.customFieldMask) { + if (projection === 'custom' && qs.customFieldMask) { qs.customFieldMask = (qs.customFieldMask as string[]).join(','); } @@ -770,6 +778,7 @@ export class GSuiteAdmin implements INodeType { returnData.push(...executionErrorData); continue; } + throw error; throw new NodeOperationError( this.getNode(), `Operation "${operation}" failed for resource "${resource}".`, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index ad988b558ec33..8dccdbf9f74bd 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -68,18 +68,11 @@ export async function googleApiRequestAllItems( } /* listSearch methods */ -export async function searchUsers( - this: ILoadOptionsFunctions, - filter?: string, -): Promise { +export async function searchUsers(this: ILoadOptionsFunctions): Promise { const qs: IDataObject = { customer: 'my_customer', }; - if (filter) { - qs.query = `name:${filter}* OR email:${filter}*`; - } - // Perform the API request to list all users const responseData = await googleApiRequestAllItems.call( this, @@ -107,18 +100,11 @@ export async function searchUsers( return { results }; } -export async function searchGroups( - this: ILoadOptionsFunctions, - filter?: string, -): Promise { +export async function searchGroups(this: ILoadOptionsFunctions): Promise { const qs: IDataObject = { customer: 'my_customer', }; - if (filter) { - qs.query = `name:${filter}* OR email:${filter}*`; - } - // Perform the API request to list all groups const responseData = await googleApiRequestAllItems.call( this, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts index 2aceb545fc885..0d19366ac75e7 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts @@ -129,7 +129,6 @@ export const groupFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchGroups', - searchable: true, }, }, { @@ -169,7 +168,6 @@ export const groupFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchGroups', - searchable: true, }, }, { @@ -247,9 +245,10 @@ export const groupFields: INodeProperties[] = [ displayName: 'Query', name: 'query', type: 'string', - placeholder: 'e.g. name:Admins', + placeholder: 'e.g. name:contact* email:contact*', default: '', - description: 'Query string to filter the results. Follow Google Admin SDK documentation.', + description: + 'Query string to filter the results. Follow Google Admin SDK documentation .', }, { displayName: 'User ID', @@ -335,7 +334,6 @@ export const groupFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchGroups', - searchable: true, }, }, { diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index d4853a42928b1..cb04b9dc0717a 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -86,7 +86,6 @@ export const userFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchUsers', - searchable: true, }, }, { @@ -137,7 +136,6 @@ export const userFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchGroups', - searchable: true, }, }, { @@ -577,7 +575,6 @@ export const userFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchUsers', - searchable: true, }, }, { @@ -633,7 +630,6 @@ export const userFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchUsers', - searchable: true, }, }, { @@ -750,6 +746,7 @@ export const userFields: INodeProperties[] = [ displayName: 'Custom Schema Names or IDs', name: 'customFieldMask', type: 'multiOptions', + required: true, displayOptions: { show: { operation: ['get'], @@ -887,6 +884,7 @@ export const userFields: INodeProperties[] = [ displayName: 'Custom Schema Names or IDs', name: 'customFieldMask', type: 'multiOptions', + required: true, displayOptions: { show: { operation: ['getAll'], @@ -932,9 +930,10 @@ export const userFields: INodeProperties[] = [ displayName: 'Query', name: 'query', type: 'string', - placeholder: 'e.g. name:Admins', + placeholder: 'e.g. name:contact* email:contact*', default: '', - description: 'Query string to filter the results. Follow Google Admin SDK documentation.', + description: + 'Query string to filter the results. Follow Google Admin SDK documentation .', }, { displayName: 'Show Deleted', @@ -1033,7 +1032,6 @@ export const userFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchUsers', - searchable: true, }, }, { @@ -1084,7 +1082,6 @@ export const userFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchGroups', - searchable: true, }, }, { @@ -1121,7 +1118,6 @@ export const userFields: INodeProperties[] = [ type: 'list', typeOptions: { searchListMethod: 'searchUsers', - searchable: true, }, }, { From 227de7a8d34cbac0c11289fd84cdb5bb0a616eee Mon Sep 17 00:00:00 2001 From: Stamsy Date: Tue, 7 Jan 2025 15:39:28 +0200 Subject: [PATCH 13/39] add error description --- .../nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 874b31321faff..9c852a7640fb7 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -7,7 +7,7 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { googleApiRequest, @@ -213,7 +213,8 @@ export class GSuiteAdmin implements INodeType { // Validate the query format const regex = /^(name|email):\S+$/; if (!regex.test(query)) { - throw new ApplicationError( + throw new NodeOperationError( + this.getNode(), 'Invalid query format. Query must follow the format "displayName:" or "email:".', ); } @@ -316,7 +317,8 @@ export class GSuiteAdmin implements INodeType { } if (!userEmail) { - throw new ApplicationError( + throw new NodeOperationError( + this.getNode(), 'Unable to determine the user email for adding to the group.', ); } @@ -778,12 +780,12 @@ export class GSuiteAdmin implements INodeType { returnData.push(...executionErrorData); continue; } - throw error; throw new NodeOperationError( this.getNode(), `Operation "${operation}" failed for resource "${resource}".`, + { - description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.message}`, + description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.description}`, itemIndex: i, }, ); From b5fa50a802338507137cd7ce42410b4174850ff4 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Thu, 9 Jan 2025 00:39:18 +0200 Subject: [PATCH 14/39] add unit tests --- .../GSuiteAdmin/test/GoogleApiRequest.test.ts | 124 +++++++++++++++ .../test/GoogleApiRequestAllItems.test.ts | 147 ++++++++++++++++++ .../GSuiteAdmin/test/SearchGroups.test.ts | 99 ++++++++++++ .../GSuiteAdmin/test/SearchUsers.test.ts | 101 ++++++++++++ 4 files changed, 471 insertions(+) create mode 100644 packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts create mode 100644 packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts create mode 100644 packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts create mode 100644 packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts new file mode 100644 index 0000000000000..6986beb8a3ff5 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts @@ -0,0 +1,124 @@ +import type { + IExecuteFunctions, + ILoadOptionsFunctions, + IDataObject, + IHttpRequestMethods, + IRequestOptions, + JsonObject, +} from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; + +describe('googleApiRequest', () => { + let mockContext: IExecuteFunctions | ILoadOptionsFunctions; + let googleApiRequest: ( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + resource: string, + body?: any, + qs?: IDataObject, + uri?: string, + headers?: IDataObject, + ) => Promise; + + beforeEach(() => { + mockContext = { + helpers: { + requestOAuth2: jest.fn(), + }, + getNode: jest.fn(), + } as unknown as IExecuteFunctions | ILoadOptionsFunctions; + + googleApiRequest = async function ( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + resource: string, + body: any = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {}, + ): Promise { + const options: IRequestOptions = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com/admin${resource}`, + json: true, + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body as IDataObject).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'gSuiteAdminOAuth2Api', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } + }; + + jest.clearAllMocks(); + }); + + it('should make a successful API request with default options', async () => { + (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ success: true }); + + const result = await googleApiRequest.call(mockContext, 'GET', '/example/resource'); + + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledWith( + 'gSuiteAdminOAuth2Api', + expect.objectContaining({ + method: 'GET', + uri: 'https://www.googleapis.com/admin/example/resource', + headers: { 'Content-Type': 'application/json' }, + json: true, + }), + ); + expect(result).toEqual({ success: true }); + }); + + it('should handle additional headers', async () => { + (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ success: true }); + + await googleApiRequest.call(mockContext, 'POST', '/example/resource', {}, {}, undefined, { + Authorization: 'Bearer token', + }); + + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledWith( + 'gSuiteAdminOAuth2Api', + expect.objectContaining({ + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + }, + }), + ); + }); + + it('should omit the body if it is empty', async () => { + (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ success: true }); + + await googleApiRequest.call(mockContext, 'GET', '/example/resource', {}); + + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledWith( + 'gSuiteAdminOAuth2Api', + expect.not.objectContaining({ body: expect.anything() }), + ); + }); + + it('should throw a NodeApiError if the request fails', async () => { + const errorResponse = { message: 'API Error' }; + (mockContext.helpers.requestOAuth2 as jest.Mock).mockRejectedValueOnce(errorResponse); + + await expect(googleApiRequest.call(mockContext, 'GET', '/example/resource')).rejects.toThrow( + NodeApiError, + ); + + expect(mockContext.getNode).toHaveBeenCalled(); + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalled(); + }); +}); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts new file mode 100644 index 0000000000000..9639a6e74fd01 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts @@ -0,0 +1,147 @@ +import type { IDataObject, IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow'; + +import { googleApiRequest } from '../GenericFunctions'; + +jest.mock('../GenericFunctions', () => ({ + googleApiRequest: jest.fn(), +})); + +describe('googleApiRequestAllItems', () => { + let mockContext: IExecuteFunctions | ILoadOptionsFunctions; + let googleApiRequestAllItems: ( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', + endpoint: string, + body?: any, + query?: IDataObject, + ) => Promise; + + beforeEach(() => { + mockContext = { + requestWithAuthentication: jest.fn(), + } as unknown as IExecuteFunctions | ILoadOptionsFunctions; + + googleApiRequestAllItems = async function ( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', + endpoint: string, + body: any = {}, + query: IDataObject = {}, + ): Promise { + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData.nextPageToken; + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + } while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== ''); + + return returnData; + }; + + (googleApiRequest as jest.Mock).mockReset(); + }); + + it('should handle a single page of results', async () => { + (googleApiRequest as jest.Mock).mockResolvedValueOnce({ + users: [ + { id: '1', name: 'User 1' }, + { id: '2', name: 'User 2' }, + ], + }); + + const result = await googleApiRequestAllItems.call( + mockContext, + 'users', + 'GET', + '/directory/v1/users', + ); + + expect(result).toEqual([ + { id: '1', name: 'User 1' }, + { id: '2', name: 'User 2' }, + ]); + expect(googleApiRequest).toHaveBeenCalledTimes(1); + }); + + it('should handle multiple pages of results', async () => { + (googleApiRequest as jest.Mock) + .mockResolvedValueOnce({ + users: [{ id: '1', name: 'User 1' }], + nextPageToken: 'page2', + }) + .mockResolvedValueOnce({ + users: [{ id: '2', name: 'User 2' }], + nextPageToken: 'page3', + }) + .mockResolvedValueOnce({ + users: [{ id: '3', name: 'User 3' }], + }); + + const result = await googleApiRequestAllItems.call( + mockContext, + 'users', + 'GET', + '/directory/v1/users', + ); + + expect(result).toEqual([ + { id: '1', name: 'User 1' }, + { id: '2', name: 'User 2' }, + { id: '3', name: 'User 3' }, + ]); + expect(googleApiRequest).toHaveBeenCalledTimes(3); + }); + + it('should return an empty array if no results are found', async () => { + (googleApiRequest as jest.Mock).mockResolvedValueOnce({ + users: [], + }); + + const result = await googleApiRequestAllItems.call( + mockContext, + 'users', + 'GET', + '/directory/v1/users', + ); + + expect(result).toEqual([]); + expect(googleApiRequest).toHaveBeenCalledTimes(1); + }); + + it('should handle missing propertyName gracefully', async () => { + (googleApiRequest as jest.Mock).mockResolvedValueOnce({}); + + const result = await googleApiRequestAllItems.call( + mockContext, + 'missingProperty', + 'GET', + '/directory/v1/users', + ); + + expect(result).toEqual([]); + expect(googleApiRequest).toHaveBeenCalledTimes(1); + }); + + it('should stop fetching when nextPageToken is undefined or empty', async () => { + (googleApiRequest as jest.Mock).mockResolvedValueOnce({ + users: [{ id: '1', name: 'User 1' }], + nextPageToken: '', + }); + + const result = await googleApiRequestAllItems.call( + mockContext, + 'users', + 'GET', + '/directory/v1/users', + ); + + expect(result).toEqual([{ id: '1', name: 'User 1' }]); + expect(googleApiRequest).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts new file mode 100644 index 0000000000000..ef7c0cd6d69e4 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts @@ -0,0 +1,99 @@ +import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; + +import { googleApiRequestAllItems } from '../GenericFunctions'; + +jest.mock('../GenericFunctions', () => ({ + googleApiRequestAllItems: jest.fn(), +})); + +describe('searchGroups', () => { + let mockContext: ILoadOptionsFunctions; + let searchGroups: (this: ILoadOptionsFunctions) => Promise; + + beforeEach(() => { + mockContext = { + requestWithAuthentication: jest.fn(), + } as unknown as ILoadOptionsFunctions; + + searchGroups = async function (this: ILoadOptionsFunctions): Promise { + const qs = { + customer: 'my_customer', + }; + + const responseData = await googleApiRequestAllItems.call( + this, + 'groups', + 'GET', + '/directory/v1/groups', + {}, + qs, + ); + + if (!responseData || responseData.length === 0) { + console.warn('No groups found in the response'); + return { results: [] }; + } + + const results = responseData.map((group: { name?: string; email?: string; id?: string }) => ({ + name: group.name || group.email || 'Unnamed Group', + value: group.id || group.email, + })); + + return { results }; + }; + + (googleApiRequestAllItems as jest.Mock).mockReset(); + }); + + it('should return a list of groups when API responds with groups', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ + { name: 'Admins', email: 'admins@example.com', id: '1' }, + { name: 'Developers', email: 'developers@example.com', id: '2' }, + ]); + + const result = await searchGroups.call(mockContext); + + expect(result.results).toHaveLength(2); + expect(result.results).toEqual([ + { name: 'Admins', value: '1' }, + { name: 'Developers', value: '2' }, + ]); + }); + + it('should return an empty array when API responds with no groups', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + + const result = await searchGroups.call(mockContext); + + expect(result.results).toEqual([]); + }); + + it('should handle missing fields gracefully', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ + { email: 'admins@example.com', id: '1' }, + { name: 'Developers', id: '2' }, + {}, + ]); + + const result = await searchGroups.call(mockContext); + + expect(result.results).toEqual([ + { name: 'admins@example.com', value: '1' }, + { name: 'Developers', value: '2' }, + { name: 'Unnamed Group', value: undefined }, + ]); + }); + + it('should warn and return an empty array when no groups are found', async () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + + const result = await searchGroups.call(mockContext); + + expect(consoleSpy).toHaveBeenCalledWith('No groups found in the response'); + expect(result.results).toEqual([]); + + consoleSpy.mockRestore(); + }); +}); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts new file mode 100644 index 0000000000000..b7b1cb4bd8674 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts @@ -0,0 +1,101 @@ +import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; + +import { googleApiRequestAllItems } from '../GenericFunctions'; + +jest.mock('../GenericFunctions', () => ({ + googleApiRequestAllItems: jest.fn(), +})); + +describe('searchUsers', () => { + let mockContext: ILoadOptionsFunctions; + let searchUsers: (this: ILoadOptionsFunctions) => Promise; + + beforeEach(() => { + mockContext = { + requestWithAuthentication: jest.fn(), + } as unknown as ILoadOptionsFunctions; + + searchUsers = async function (this: ILoadOptionsFunctions): Promise { + const qs = { + customer: 'my_customer', + }; + + const responseData = await googleApiRequestAllItems.call( + this, + 'users', + 'GET', + '/directory/v1/users', + {}, + qs, + ); + + if (!responseData || responseData.length === 0) { + console.warn('No users found in the response'); + return { results: [] }; + } + + const results = responseData.map( + (user: { name?: { fullName?: string }; primaryEmail?: string; id?: string }) => ({ + name: user.name?.fullName || user.primaryEmail || 'Unnamed User', + value: user.id || user.primaryEmail, + }), + ); + + return { results }; + }; + + (googleApiRequestAllItems as jest.Mock).mockReset(); + }); + + it('should return a list of users when API responds with users', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ + { name: { fullName: 'John Doe' }, primaryEmail: 'john.doe@example.com', id: '1' }, + { name: { fullName: 'Jane Smith' }, primaryEmail: 'jane.smith@example.com', id: '2' }, + ]); + + const result = await searchUsers.call(mockContext); + + expect(result.results).toHaveLength(2); + expect(result.results).toEqual([ + { name: 'John Doe', value: '1' }, + { name: 'Jane Smith', value: '2' }, + ]); + }); + + it('should return an empty array when API responds with no users', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + + const result = await searchUsers.call(mockContext); + + expect(result.results).toEqual([]); + }); + + it('should handle missing fields gracefully', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ + { primaryEmail: 'john.doe@example.com', id: '1' }, + { name: { fullName: 'Jane Smith' }, id: '2' }, + {}, + ]); + + const result = await searchUsers.call(mockContext); + + expect(result.results).toEqual([ + { name: 'john.doe@example.com', value: '1' }, + { name: 'Jane Smith', value: '2' }, + { name: 'Unnamed User', value: undefined }, + ]); + }); + + it('should warn and return an empty array when no users are found', async () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + + const result = await searchUsers.call(mockContext); + + expect(consoleSpy).toHaveBeenCalledWith('No users found in the response'); + expect(result.results).toEqual([]); + + consoleSpy.mockRestore(); + }); +}); From 9bd10c4eda765d707de322e036d2b7681799b935 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Fri, 10 Jan 2025 22:16:56 +0200 Subject: [PATCH 15/39] update tests --- .../GSuiteAdmin/test/GoogleApiRequest.test.ts | 53 +----- .../test/GoogleApiRequestAllItems.test.ts | 151 ++++++------------ .../GSuiteAdmin/test/SearchGroups.test.ts | 111 +++++-------- .../GSuiteAdmin/test/SearchUsers.test.ts | 106 +++++------- 4 files changed, 124 insertions(+), 297 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts index 6986beb8a3ff5..5e7ba876288ef 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts @@ -1,24 +1,10 @@ -import type { - IExecuteFunctions, - ILoadOptionsFunctions, - IDataObject, - IHttpRequestMethods, - IRequestOptions, - JsonObject, -} from 'n8n-workflow'; +import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; +import { googleApiRequest } from '../GenericFunctions'; + describe('googleApiRequest', () => { let mockContext: IExecuteFunctions | ILoadOptionsFunctions; - let googleApiRequest: ( - this: IExecuteFunctions | ILoadOptionsFunctions, - method: IHttpRequestMethods, - resource: string, - body?: any, - qs?: IDataObject, - uri?: string, - headers?: IDataObject, - ) => Promise; beforeEach(() => { mockContext = { @@ -28,39 +14,6 @@ describe('googleApiRequest', () => { getNode: jest.fn(), } as unknown as IExecuteFunctions | ILoadOptionsFunctions; - googleApiRequest = async function ( - this: IExecuteFunctions | ILoadOptionsFunctions, - method: IHttpRequestMethods, - resource: string, - body: any = {}, - qs: IDataObject = {}, - uri?: string, - headers: IDataObject = {}, - ): Promise { - const options: IRequestOptions = { - headers: { - 'Content-Type': 'application/json', - }, - method, - body, - qs, - uri: uri || `https://www.googleapis.com/admin${resource}`, - json: true, - }; - try { - if (Object.keys(headers).length !== 0) { - options.headers = Object.assign({}, options.headers, headers); - } - if (Object.keys(body as IDataObject).length === 0) { - delete options.body; - } - //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'gSuiteAdminOAuth2Api', options); - } catch (error) { - throw new NodeApiError(this.getNode(), error as JsonObject); - } - }; - jest.clearAllMocks(); }); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts index 9639a6e74fd01..7674ab7933177 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts @@ -1,147 +1,86 @@ -import type { IDataObject, IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow'; - -import { googleApiRequest } from '../GenericFunctions'; - -jest.mock('../GenericFunctions', () => ({ - googleApiRequest: jest.fn(), -})); +import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow'; +import { googleApiRequestAllItems } from '../GenericFunctions'; describe('googleApiRequestAllItems', () => { let mockContext: IExecuteFunctions | ILoadOptionsFunctions; - let googleApiRequestAllItems: ( - this: IExecuteFunctions | ILoadOptionsFunctions, - propertyName: string, - method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', - endpoint: string, - body?: any, - query?: IDataObject, - ) => Promise; beforeEach(() => { mockContext = { - requestWithAuthentication: jest.fn(), + helpers: { + requestOAuth2: jest.fn(), + }, + getNode: jest.fn(), } as unknown as IExecuteFunctions | ILoadOptionsFunctions; - googleApiRequestAllItems = async function ( - this: IExecuteFunctions | ILoadOptionsFunctions, - propertyName: string, - method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', - endpoint: string, - body: any = {}, - query: IDataObject = {}, - ): Promise { - const returnData: IDataObject[] = []; - - let responseData; - query.maxResults = 100; - - do { - responseData = await googleApiRequest.call(this, method, endpoint, body, query); - query.pageToken = responseData.nextPageToken; - returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); - } while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== ''); - - return returnData; - }; - - (googleApiRequest as jest.Mock).mockReset(); + jest.clearAllMocks(); }); - - it('should handle a single page of results', async () => { - (googleApiRequest as jest.Mock).mockResolvedValueOnce({ - users: [ - { id: '1', name: 'User 1' }, - { id: '2', name: 'User 2' }, - ], + it('should return all items across multiple pages', async () => { + (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ + nextPageToken: '', + items: [{ id: '1' }, { id: '2' }], }); const result = await googleApiRequestAllItems.call( mockContext, - 'users', + 'items', 'GET', - '/directory/v1/users', + '/example/resource', ); - expect(result).toEqual([ - { id: '1', name: 'User 1' }, - { id: '2', name: 'User 2' }, - ]); - expect(googleApiRequest).toHaveBeenCalledTimes(1); - }); - - it('should handle multiple pages of results', async () => { - (googleApiRequest as jest.Mock) - .mockResolvedValueOnce({ - users: [{ id: '1', name: 'User 1' }], - nextPageToken: 'page2', - }) - .mockResolvedValueOnce({ - users: [{ id: '2', name: 'User 2' }], - nextPageToken: 'page3', - }) - .mockResolvedValueOnce({ - users: [{ id: '3', name: 'User 3' }], - }); - - const result = await googleApiRequestAllItems.call( - mockContext, - 'users', - 'GET', - '/directory/v1/users', + expect(result).toEqual([{ id: '1' }, { id: '2' }]); + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledTimes(1); + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledWith( + 'gSuiteAdminOAuth2Api', + expect.objectContaining({ + method: 'GET', + qs: expect.objectContaining({ pageToken: '', maxResults: 100 }), + }), ); - - expect(result).toEqual([ - { id: '1', name: 'User 1' }, - { id: '2', name: 'User 2' }, - { id: '3', name: 'User 3' }, - ]); - expect(googleApiRequest).toHaveBeenCalledTimes(3); }); - it('should return an empty array if no results are found', async () => { - (googleApiRequest as jest.Mock).mockResolvedValueOnce({ - users: [], + it('should handle single-page responses', async () => { + (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ + nextPageToken: '', + items: [{ id: '1' }, { id: '2' }], }); const result = await googleApiRequestAllItems.call( mockContext, - 'users', + 'items', 'GET', - '/directory/v1/users', + '/example/resource', ); - expect(result).toEqual([]); - expect(googleApiRequest).toHaveBeenCalledTimes(1); + expect(result).toEqual([{ id: '1' }, { id: '2' }]); + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledTimes(1); }); - it('should handle missing propertyName gracefully', async () => { - (googleApiRequest as jest.Mock).mockResolvedValueOnce({}); + it('should handle empty responses gracefully', async () => { + (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ + nextPageToken: '', + items: [], + }); const result = await googleApiRequestAllItems.call( mockContext, - 'missingProperty', + 'items', 'GET', - '/directory/v1/users', + '/example/resource', ); expect(result).toEqual([]); - expect(googleApiRequest).toHaveBeenCalledTimes(1); + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledTimes(1); }); - it('should stop fetching when nextPageToken is undefined or empty', async () => { - (googleApiRequest as jest.Mock).mockResolvedValueOnce({ - users: [{ id: '1', name: 'User 1' }], - nextPageToken: '', - }); + it('should throw a NodeApiError if a request fails', async () => { + const errorResponse = { message: 'API Error' }; + (mockContext.helpers.requestOAuth2 as jest.Mock).mockRejectedValueOnce(errorResponse); - const result = await googleApiRequestAllItems.call( - mockContext, - 'users', - 'GET', - '/directory/v1/users', - ); + await expect( + googleApiRequestAllItems.call(mockContext, 'items', 'GET', '/example/resource'), + ).rejects.toThrow(); - expect(result).toEqual([{ id: '1', name: 'User 1' }]); - expect(googleApiRequest).toHaveBeenCalledTimes(1); + expect(mockContext.getNode).toHaveBeenCalled(); + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts index ef7c0cd6d69e4..c24b407ad676e 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts @@ -1,99 +1,60 @@ -import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; +import type { ILoadOptionsFunctions } from 'n8n-workflow'; +import { searchGroups } from '../GenericFunctions'; -import { googleApiRequestAllItems } from '../GenericFunctions'; +describe('GenericFunctions - searchGroups', () => { + const mockGoogleApiRequestAllItems = jest.fn(); -jest.mock('../GenericFunctions', () => ({ - googleApiRequestAllItems: jest.fn(), -})); - -describe('searchGroups', () => { - let mockContext: ILoadOptionsFunctions; - let searchGroups: (this: ILoadOptionsFunctions) => Promise; + const mockContext = { + helpers: { + requestOAuth2: mockGoogleApiRequestAllItems, + }, + } as unknown as ILoadOptionsFunctions; beforeEach(() => { - mockContext = { - requestWithAuthentication: jest.fn(), - } as unknown as ILoadOptionsFunctions; - - searchGroups = async function (this: ILoadOptionsFunctions): Promise { - const qs = { - customer: 'my_customer', - }; - - const responseData = await googleApiRequestAllItems.call( - this, - 'groups', - 'GET', - '/directory/v1/groups', - {}, - qs, - ); - - if (!responseData || responseData.length === 0) { - console.warn('No groups found in the response'); - return { results: [] }; - } - - const results = responseData.map((group: { name?: string; email?: string; id?: string }) => ({ - name: group.name || group.email || 'Unnamed Group', - value: group.id || group.email, - })); - - return { results }; - }; - - (googleApiRequestAllItems as jest.Mock).mockReset(); + mockGoogleApiRequestAllItems.mockClear(); }); - + //TODO - this test not works it('should return a list of groups when API responds with groups', async () => { - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ - { name: 'Admins', email: 'admins@example.com', id: '1' }, - { name: 'Developers', email: 'developers@example.com', id: '2' }, + mockGoogleApiRequestAllItems.mockResolvedValue([ + { + kind: 'admin#directory#group', + id: '01302m922pmp3e4', + email: 'new3@digital-boss.de', + name: 'New2', + description: 'new1', + }, + { + kind: 'admin#directory#group', + id: '01x0gk373c9z46j', + email: 'newoness@digital-boss.de', + name: 'NewOness', + description: 'test', + }, ]); const result = await searchGroups.call(mockContext); - expect(result.results).toHaveLength(2); - expect(result.results).toEqual([ - { name: 'Admins', value: '1' }, - { name: 'Developers', value: '2' }, - ]); + expect(result).toEqual({ + results: [ + { name: 'New2', value: '01302m922pmp3e4' }, + { name: 'NewOness', value: '01x0gk373c9z46j' }, + ], + }); }); it('should return an empty array when API responds with no groups', async () => { - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + mockGoogleApiRequestAllItems.mockResolvedValue([]); const result = await searchGroups.call(mockContext); - expect(result.results).toEqual([]); - }); - - it('should handle missing fields gracefully', async () => { - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ - { email: 'admins@example.com', id: '1' }, - { name: 'Developers', id: '2' }, - {}, - ]); - - const result = await searchGroups.call(mockContext); - - expect(result.results).toEqual([ - { name: 'admins@example.com', value: '1' }, - { name: 'Developers', value: '2' }, - { name: 'Unnamed Group', value: undefined }, - ]); + expect(result).toEqual({ results: [] }); }); it('should warn and return an empty array when no groups are found', async () => { - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); - - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + mockGoogleApiRequestAllItems.mockResolvedValue([]); const result = await searchGroups.call(mockContext); - expect(consoleSpy).toHaveBeenCalledWith('No groups found in the response'); - expect(result.results).toEqual([]); - - consoleSpy.mockRestore(); + expect(result).toEqual({ results: [] }); }); }); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts index b7b1cb4bd8674..cd8459e984cdc 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts @@ -1,77 +1,54 @@ -import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; +import type { ILoadOptionsFunctions } from 'n8n-workflow'; +import { searchUsers } from '../GenericFunctions'; -import { googleApiRequestAllItems } from '../GenericFunctions'; +describe('GenericFunctions - searchUsers', () => { + const mockGoogleApiRequestAllItems = jest.fn(); -jest.mock('../GenericFunctions', () => ({ - googleApiRequestAllItems: jest.fn(), -})); - -describe('searchUsers', () => { - let mockContext: ILoadOptionsFunctions; - let searchUsers: (this: ILoadOptionsFunctions) => Promise; + const mockContext = { + helpers: { + requestOAuth2: mockGoogleApiRequestAllItems, + }, + } as unknown as ILoadOptionsFunctions; beforeEach(() => { - mockContext = { - requestWithAuthentication: jest.fn(), - } as unknown as ILoadOptionsFunctions; - - searchUsers = async function (this: ILoadOptionsFunctions): Promise { - const qs = { - customer: 'my_customer', - }; - - const responseData = await googleApiRequestAllItems.call( - this, - 'users', - 'GET', - '/directory/v1/users', - {}, - qs, - ); - - if (!responseData || responseData.length === 0) { - console.warn('No users found in the response'); - return { results: [] }; - } - - const results = responseData.map( - (user: { name?: { fullName?: string }; primaryEmail?: string; id?: string }) => ({ - name: user.name?.fullName || user.primaryEmail || 'Unnamed User', - value: user.id || user.primaryEmail, - }), - ); - - return { results }; - }; - - (googleApiRequestAllItems as jest.Mock).mockReset(); + mockGoogleApiRequestAllItems.mockClear(); }); + //TODO - this test need to be fixed it('should return a list of users when API responds with users', async () => { - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ - { name: { fullName: 'John Doe' }, primaryEmail: 'john.doe@example.com', id: '1' }, - { name: { fullName: 'Jane Smith' }, primaryEmail: 'jane.smith@example.com', id: '2' }, + mockGoogleApiRequestAllItems.mockResolvedValue([ + { + id: '1', + name: { fullName: 'John Doe' }, + primaryEmail: 'john.doe@example.com', + }, + { + id: '2', + name: { fullName: 'Jane Smith' }, + primaryEmail: 'jane.smith@example.com', + }, ]); const result = await searchUsers.call(mockContext); - expect(result.results).toHaveLength(2); - expect(result.results).toEqual([ - { name: 'John Doe', value: '1' }, - { name: 'Jane Smith', value: '2' }, - ]); + expect(result).toEqual({ + results: [ + { name: 'John Doe', value: '1' }, + { name: 'Jane Smith', value: '2' }, + ], + }); }); it('should return an empty array when API responds with no users', async () => { - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + mockGoogleApiRequestAllItems.mockResolvedValue([]); const result = await searchUsers.call(mockContext); - expect(result.results).toEqual([]); + expect(result).toEqual({ results: [] }); }); it('should handle missing fields gracefully', async () => { - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ + mockGoogleApiRequestAllItems.mockResolvedValue([ { primaryEmail: 'john.doe@example.com', id: '1' }, { name: { fullName: 'Jane Smith' }, id: '2' }, {}, @@ -79,23 +56,20 @@ describe('searchUsers', () => { const result = await searchUsers.call(mockContext); - expect(result.results).toEqual([ - { name: 'john.doe@example.com', value: '1' }, - { name: 'Jane Smith', value: '2' }, - { name: 'Unnamed User', value: undefined }, - ]); + expect(result).toEqual({ + results: [ + { name: 'john.doe@example.com', value: '1' }, + { name: 'Jane Smith', value: '2' }, + { name: 'Unnamed User', value: undefined }, + ], + }); }); it('should warn and return an empty array when no users are found', async () => { - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); - - (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); + mockGoogleApiRequestAllItems.mockResolvedValue([]); const result = await searchUsers.call(mockContext); - expect(consoleSpy).toHaveBeenCalledWith('No users found in the response'); - expect(result.results).toEqual([]); - - consoleSpy.mockRestore(); + expect(result).toEqual({ results: [] }); }); }); From f444a576f3ceaf6fc83e8697e8ba0e4a9e02344a Mon Sep 17 00:00:00 2001 From: Stamsy Date: Fri, 10 Jan 2025 22:34:12 +0200 Subject: [PATCH 16/39] update device resource and add custom fields logic --- .../Google/GSuiteAdmin/DeviceDescription.ts | 11 +- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 165 ++++++++++++++---- .../Google/GSuiteAdmin/UserDescription.ts | 26 ++- 3 files changed, 163 insertions(+), 39 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index afc4abed6b80e..8979c28af8718 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -46,8 +46,8 @@ export const deviceFields: INodeProperties[] = [ /* device:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'UUID', - name: 'uuid', + displayName: 'Device ID', + name: 'deviceId', type: 'string', required: true, displayOptions: { @@ -217,6 +217,9 @@ export const deviceFields: INodeProperties[] = [ }, ], }, + /* -------------------------------------------------------------------------- */ + /* device:update...... */ + /* -------------------------------------------------------------------------- */ { displayName: 'Update Fields', name: 'updateOptions', @@ -275,6 +278,10 @@ export const deviceFields: INodeProperties[] = [ }, ], }, + + /* -------------------------------------------------------------------------- */ + /* device:changeStatus */ + /* -------------------------------------------------------------------------- */ { displayName: 'Action', name: 'action', diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 9c852a7640fb7..c6c99dc64502d 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -7,8 +7,9 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import { deviceFields, deviceOperations } from './DeviceDescription'; import { googleApiRequest, googleApiRequestAllItems, @@ -17,7 +18,6 @@ import { } from './GenericFunctions'; import { groupFields, groupOperations } from './GroupDescripion'; import { userFields, userOperations } from './UserDescription'; -import { deviceFields, deviceOperations } from './DeviceDescription'; export class GSuiteAdmin implements INodeType { description: INodeTypeDescription = { @@ -94,36 +94,87 @@ export class GSuiteAdmin implements INodeType { }, //Get all the schemas async getSchemas(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; const schemas = await googleApiRequestAllItems.call( this, 'schemas', 'GET', '/directory/v1/customer/my_customer/schemas', ); - for (const schema of schemas) { - const schemaName = schema.displayName; - const schemaId = schema.schemaName; - returnData.push({ - name: schemaName, - value: schemaId, - }); + return schemas.map((schema: { schemaName: string; displayName: string }) => ({ + name: schema.displayName || schema.schemaName, + value: schema.schemaName, + })); + }, + async getSchemaFields(this: ILoadOptionsFunctions): Promise { + const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject; + console.log('additionalFields', JSON.stringify(additionalFields)); + + if (additionalFields.customFields) { + } else { + console.warn('No Custom Fields found in additionalFields.'); + return [ + { + name: 'Please Select a Schema First.', + value: '', + }, + ]; } - return returnData; + const customFields = (additionalFields.customFields as IDataObject) + .fieldValues as IDataObject[]; + console.log('customFields', customFields); + const schemaName = customFields?.[0]?.schemaName as string; + console.log('Schema Name:', schemaName); + + try { + const schema = await googleApiRequest.call( + this, + 'GET', + `/directory/v1/customer/my_customer/schemas/${schemaName}`, + //'/directory/v1/customer/my_customer/schemas/new', + ); + + if (schema.fields && Array.isArray(schema.fields)) { + return schema.fields.map((field: { fieldName: string; displayName: string }) => ({ + name: field.displayName || field.fieldName, + value: field.fieldName, + })); + } else { + console.warn('No fields found for the schema.'); + } + } catch (error) { + console.error('Error fetching schema fields:', error); + } + + return []; }, async getOrgUnits(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const orgUnits = await googleApiRequest.call( this, 'GET', - '/directory/v1/customer/my_customer/orgunits?orgUnitPath=/&type=all', + '/directory/v1/customer/my_customer/orgunits', + {}, + { orgUnitPath: '/', type: 'all' }, ); - for (const orgUnit of orgUnits.organizationUnits) { - const orgUnitName = orgUnit.orgUnitPath; - returnData.push({ - name: orgUnitName, - value: orgUnitName, - }); + + // Check if organizationUnits exist and are iterable + if (orgUnits.organizationUnits && Array.isArray(orgUnits.organizationUnits)) { + if (orgUnits.organizationUnits.length === 0) { + throw new ApplicationError( + 'No organizational units found. Please create organizational units in the Google Admin Console under "Directory > Organizational units".', + ); + } + + for (const unit of orgUnits.organizationUnits) { + returnData.push({ + name: unit.name, + value: unit.orgUnitPath, + }); + } + } else { + throw new ApplicationError( + 'Failed to retrieve organizational units. Ensure your account has organizational units configured.', + ); } return returnData; }, @@ -207,7 +258,16 @@ export class GSuiteAdmin implements INodeType { const returnAll = this.getNodeParameter('returnAll', i); const filter = this.getNodeParameter('filter', i, {}) as IDataObject; const sort = this.getNodeParameter('sort', i, {}) as IDataObject; - if (typeof filter.query === 'string') { + + if (filter.customer) { + qs.customer = filter.customer as string; + } + + if (filter.domain) { + qs.domain = filter.domain as string; + } + + if (filter.query && typeof filter.query === 'string') { const query = filter.query.trim(); // Validate the query format @@ -222,6 +282,10 @@ export class GSuiteAdmin implements INodeType { qs.query = query; } + if (filter.userId) { + qs.userId = filter.userId as string; + } + // Handle sort options if (sort.sortRules) { const { orderBy, sortOrder } = sort.sortRules as { @@ -389,6 +453,35 @@ export class GSuiteAdmin implements INodeType { }; } + if (additionalFields.customFields) { + const customFields = (additionalFields.customFields as IDataObject) + .fieldValues as IDataObject[]; + console.log('Custom Fields:', customFields); + const customSchemas: IDataObject = {}; + customFields.forEach((field) => { + const { schemaName, fieldName, value } = field as { + schemaName: string; + fieldName: string; + value: any; + }; + + if (!schemaName || !fieldName || value === undefined || value === '') { + console.error('Missing schemaName, fieldName, or value in customFields:', field); + return; + } + + if (!customSchemas[schemaName]) { + customSchemas[schemaName] = {}; + } + + (customSchemas[schemaName] as IDataObject)[fieldName] = value; + }); + + if (Object.keys(customSchemas).length > 0) { + body.customSchemas = customSchemas; + } + } + // Send the final request to create the user responseData = await googleApiRequest.call( this, 'POST', @@ -669,28 +762,29 @@ export class GSuiteAdmin implements INodeType { } if (resource === 'device') { - //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/uuid + //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/deviceId if (operation === 'get') { - const uuid = this.getNodeParameter('uuid', i) as string; + const deviceId = this.getNodeParameter('deviceId', i) as string; const projection = this.getNodeParameter('projection', 1); - // Validate uuid - if (!uuid) { + // Validate deviceId + if (!deviceId) { throw new NodeOperationError( this.getNode(), - 'uuid is required but was not provided.', + 'deviceId is required but was not provided.', { itemIndex: i }, ); } + console.log('deviceId', deviceId); responseData = await googleApiRequest.call( this, 'GET', - `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, + `/directory/v1/customer/my_customer/devices/chromeos/${deviceId}?projection=${projection}`, {}, ); } - //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/ + //https://developers.google.com/admin-sdk/directory/reference/rest/v1/chromeosdevices/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i); const projection = this.getNodeParameter('projection', 1); @@ -724,18 +818,23 @@ export class GSuiteAdmin implements INodeType { qs, ); } + if (!responseData || responseData.length === 0) { + return [this.helpers.returnJsonArray({})]; + } + + return [this.helpers.returnJsonArray(responseData)]; } if (operation === 'update') { - const uuid = this.getNodeParameter('uuid', i) as string; + const deviceId = this.getNodeParameter('deviceId', i) as string; const projection = this.getNodeParameter('projection', 1); const updateOptions = this.getNodeParameter('updateOptions', 1); - // Validate uuid - if (!uuid) { + // Validate deviceId + if (!deviceId) { throw new NodeOperationError( this.getNode(), - 'uuid is required but was not provided.', + 'deviceId is required but was not provided.', { itemIndex: i }, ); } @@ -743,20 +842,20 @@ export class GSuiteAdmin implements INodeType { responseData = await googleApiRequest.call( this, 'PUT', - `/directory/v1/customer/my_customer/devices/chromeos/${uuid}?projection=${projection}`, + `/directory/v1/customer/my_customer/devices/chromeos/${deviceId}?projection=${projection}`, qs, ); } if (operation === 'changeStatus') { - const uuid = this.getNodeParameter('uuid', i) as string; + const deviceId = this.getNodeParameter('deviceId', i) as string; const action = this.getNodeParameter('action', 1); qs.action = action; responseData = await googleApiRequest.call( this, 'POST', - `/directory/v1/customer/my_customer/devices/chromeos/${uuid}/action`, + `/directory/v1/customer/my_customer/devices/chromeos/${deviceId}/action`, qs, ); } diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index cb04b9dc0717a..9d645d6680f35 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -532,18 +532,36 @@ export const userFields: INodeProperties[] = [ displayName: 'Field', values: [ { - displayName: 'Field Name', + displayName: 'Schema Name or ID', + name: 'schemaName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSchemas', + }, + default: '', + description: + 'Select the schema to use for custom fields. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Field Name or ID', name: 'fieldName', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsDependsOn: ['schemaName.value'], + loadOptionsMethod: 'getSchemaFields', + }, default: '', - description: 'The name of the custom field', + required: true, + description: + 'Select the field from the selected schema. Choose from the list, or specify an ID using an expression.', }, { displayName: 'Value', name: 'value', type: 'string', default: '', - description: 'The value for the custom field', + required: true, + description: 'Provide a value for the selected field', }, ], }, From c727b3550901ce51c47df90a102fc2b596a5182e Mon Sep 17 00:00:00 2001 From: Stamsy Date: Mon, 13 Jan 2025 22:14:48 +0200 Subject: [PATCH 17/39] fix after review --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 78 ++++++++++++------- .../Google/GSuiteAdmin/GenericFunctions.ts | 10 +-- .../GSuiteAdmin/test/GoogleApiRequest.test.ts | 18 ----- .../test/GoogleApiRequestAllItems.test.ts | 53 ++++++++++--- .../GSuiteAdmin/test/SearchGroups.test.ts | 8 -- .../GSuiteAdmin/test/SearchUsers.test.ts | 40 +++------- 6 files changed, 108 insertions(+), 99 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index c6c99dc64502d..1790b8e4b8f60 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -114,7 +114,7 @@ export class GSuiteAdmin implements INodeType { console.warn('No Custom Fields found in additionalFields.'); return [ { - name: 'Please Select a Schema First.', + name: 'Invalid Schema: Missing schemaName.', value: '', }, ]; @@ -130,7 +130,6 @@ export class GSuiteAdmin implements INodeType { this, 'GET', `/directory/v1/customer/my_customer/schemas/${schemaName}`, - //'/directory/v1/customer/my_customer/schemas/new', ); if (schema.fields && Array.isArray(schema.fields)) { @@ -139,7 +138,6 @@ export class GSuiteAdmin implements INodeType { value: field.fieldName, })); } else { - console.warn('No fields found for the schema.'); } } catch (error) { console.error('Error fetching schema fields:', error); @@ -214,9 +212,7 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/delete if (operation === 'delete') { - const groupIdRaw = this.getNodeParameter('groupId', i) as any; - const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; - + const groupId = (this.getNodeParameter('groupId', i) as IDataObject).value; if (!groupId) { throw new NodeOperationError( this.getNode(), @@ -236,8 +232,7 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/get if (operation === 'get') { - const groupIdRaw = this.getNodeParameter('groupId', i) as any; - const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + const groupId = (this.getNodeParameter('groupId', i) as IDataObject).value; if (!groupId) { throw new NodeOperationError( @@ -330,8 +325,7 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/groups/update if (operation === 'update') { - const groupIdRaw = this.getNodeParameter('groupId', i) as any; - const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + const groupId = (this.getNodeParameter('groupId', i) as IDataObject).value; if (!groupId) { throw new NodeOperationError( @@ -359,11 +353,18 @@ export class GSuiteAdmin implements INodeType { if (resource === 'user') { //https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/insert if (operation === 'addToGroup') { - const groupIdRaw = this.getNodeParameter('groupId', i) as any; - const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; + const groupId = (this.getNodeParameter('groupId', i) as IDataObject).value; + const userIdParam = this.getNodeParameter('userId', i); + const userId = + typeof userIdParam === 'string' ? userIdParam : (userIdParam as IDataObject)?.value; - const userIdRaw = this.getNodeParameter('userId', i) as any; - const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; + if (!userId || typeof userId !== 'string') { + throw new NodeOperationError( + this.getNode(), + 'Invalid or missing user ID. Please provide a valid user ID.', + { itemIndex: i }, + ); + } let userEmail: string | undefined; @@ -384,6 +385,7 @@ export class GSuiteAdmin implements INodeType { throw new NodeOperationError( this.getNode(), 'Unable to determine the user email for adding to the group.', + { itemIndex: i }, ); } @@ -456,7 +458,6 @@ export class GSuiteAdmin implements INodeType { if (additionalFields.customFields) { const customFields = (additionalFields.customFields as IDataObject) .fieldValues as IDataObject[]; - console.log('Custom Fields:', customFields); const customSchemas: IDataObject = {}; customFields.forEach((field) => { const { schemaName, fieldName, value } = field as { @@ -493,9 +494,7 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/users/delete if (operation === 'delete') { - const userIdRaw = this.getNodeParameter('userId', i) as any; - const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; - + const userId = (this.getNodeParameter('userId', i) as IDataObject).value; if (!userId) { throw new NodeOperationError( this.getNode(), @@ -516,8 +515,7 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/users/get if (operation === 'get') { - const userIdRaw = this.getNodeParameter('userId', i) as any; - const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; + const userId = (this.getNodeParameter('userId', i) as IDataObject).value; const output = this.getNodeParameter('output', i); const projection = this.getNodeParameter('projection', i); @@ -663,10 +661,8 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/delete if (operation === 'removeFromGroup') { - const groupIdRaw = this.getNodeParameter('groupId', i) as any; - const groupId = typeof groupIdRaw === 'string' ? groupIdRaw : groupIdRaw.value; - const userIdRaw = this.getNodeParameter('userId', i) as any; - const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; + const groupId = (this.getNodeParameter('groupId', i) as IDataObject).value; + const userId = (this.getNodeParameter('userId', i) as IDataObject).value; const body: IDataObject = { email: userId, role: 'MEMBER', @@ -683,8 +679,7 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/reference/users/update if (operation === 'update') { - const userIdRaw = this.getNodeParameter('userId', i) as any; - const userId = typeof userIdRaw === 'string' ? userIdRaw : userIdRaw.value; + const userId = (this.getNodeParameter('userId', i) as IDataObject).value; const updateFields = this.getNodeParameter('updateFields', i); // Validate User ID @@ -703,6 +698,7 @@ export class GSuiteAdmin implements INodeType { phones?: IDataObject[]; suspended?: boolean; roles?: { [key: string]: boolean }; + customSchemas?: IDataObject; } = {}; if (updateFields.firstName) { @@ -751,6 +747,34 @@ export class GSuiteAdmin implements INodeType { }; } + if (updateFields.customFields) { + const customFields = (updateFields.customFields as IDataObject) + .fieldValues as IDataObject[]; + const customSchemas: IDataObject = {}; + customFields.forEach((field) => { + const { schemaName, fieldName, value } = field as { + schemaName: string; + fieldName: string; + value: any; + }; + + if (!schemaName || !fieldName || value === undefined || value === '') { + console.error('Missing schemaName, fieldName, or value in customFields:', field); + return; + } + + if (!customSchemas[schemaName]) { + customSchemas[schemaName] = {}; + } + + (customSchemas[schemaName] as IDataObject)[fieldName] = value; + }); + + if (Object.keys(customSchemas).length > 0) { + body.customSchemas = customSchemas; + } + } + responseData = await googleApiRequest.call( this, 'PUT', @@ -775,7 +799,7 @@ export class GSuiteAdmin implements INodeType { { itemIndex: i }, ); } - console.log('deviceId', deviceId); + responseData = await googleApiRequest.call( this, 'GET', diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index 8dccdbf9f74bd..89ac4dbbccc30 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -85,15 +85,14 @@ export async function searchUsers(this: ILoadOptionsFunctions): Promise ({ - name: user.name?.fullName || user.primaryEmail || 'Unnamed User', - value: user.id || user.primaryEmail, + (user: { name?: { fullName?: string }; id?: string }) => ({ + name: user.name?.fullName, + value: user.id, }), ); @@ -117,7 +116,6 @@ export async function searchGroups(this: ILoadOptionsFunctions): Promise ({ name: group.name || group.email || 'Unnamed Group', - value: group.id || group.email, + value: group.id, }), ); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts index 5e7ba876288ef..a23b2b4f0c091 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequest.test.ts @@ -34,24 +34,6 @@ describe('googleApiRequest', () => { expect(result).toEqual({ success: true }); }); - it('should handle additional headers', async () => { - (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ success: true }); - - await googleApiRequest.call(mockContext, 'POST', '/example/resource', {}, {}, undefined, { - Authorization: 'Bearer token', - }); - - expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledWith( - 'gSuiteAdminOAuth2Api', - expect.objectContaining({ - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer token', - }, - }), - ); - }); - it('should omit the body if it is empty', async () => { (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ success: true }); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts index 7674ab7933177..0554412827c70 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts @@ -15,10 +15,19 @@ describe('googleApiRequestAllItems', () => { jest.clearAllMocks(); }); it('should return all items across multiple pages', async () => { - (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ - nextPageToken: '', - items: [{ id: '1' }, { id: '2' }], - }); + (mockContext.helpers.requestOAuth2 as jest.Mock) + .mockResolvedValueOnce({ + nextPageToken: 'pageToken1', + items: [{ id: '1' }, { id: '2' }], + }) + .mockResolvedValueOnce({ + nextPageToken: 'pageToken2', + items: [{ id: '3' }, { id: '4' }], + }) + .mockResolvedValueOnce({ + nextPageToken: '', + items: [{ id: '5' }], + }); const result = await googleApiRequestAllItems.call( mockContext, @@ -27,13 +36,39 @@ describe('googleApiRequestAllItems', () => { '/example/resource', ); - expect(result).toEqual([{ id: '1' }, { id: '2' }]); - expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledTimes(1); - expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledWith( + expect(result).toEqual([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }]); + expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledTimes(3); + expect(mockContext.helpers.requestOAuth2).toHaveBeenNthCalledWith( + 1, + 'gSuiteAdminOAuth2Api', + expect.objectContaining({ + method: 'GET', + qs: { maxResults: 100, pageToken: '' }, + headers: { 'Content-Type': 'application/json' }, + uri: 'https://www.googleapis.com/admin/example/resource', + json: true, + }), + ); + expect(mockContext.helpers.requestOAuth2).toHaveBeenNthCalledWith( + 2, + 'gSuiteAdminOAuth2Api', + expect.objectContaining({ + method: 'GET', + qs: { maxResults: 100, pageToken: '' }, + headers: { 'Content-Type': 'application/json' }, + uri: 'https://www.googleapis.com/admin/example/resource', + json: true, + }), + ); + expect(mockContext.helpers.requestOAuth2).toHaveBeenNthCalledWith( + 3, 'gSuiteAdminOAuth2Api', expect.objectContaining({ method: 'GET', - qs: expect.objectContaining({ pageToken: '', maxResults: 100 }), + qs: { maxResults: 100, pageToken: '' }, + headers: { 'Content-Type': 'application/json' }, + uri: 'https://www.googleapis.com/admin/example/resource', + json: true, }), ); }); @@ -55,7 +90,7 @@ describe('googleApiRequestAllItems', () => { expect(mockContext.helpers.requestOAuth2).toHaveBeenCalledTimes(1); }); - it('should handle empty responses gracefully', async () => { + it('should handle empty responses', async () => { (mockContext.helpers.requestOAuth2 as jest.Mock).mockResolvedValueOnce({ nextPageToken: '', items: [], diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts index c24b407ad676e..1e42e6c89900f 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts @@ -49,12 +49,4 @@ describe('GenericFunctions - searchGroups', () => { expect(result).toEqual({ results: [] }); }); - - it('should warn and return an empty array when no groups are found', async () => { - mockGoogleApiRequestAllItems.mockResolvedValue([]); - - const result = await searchGroups.call(mockContext); - - expect(result).toEqual({ results: [] }); - }); }); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts index cd8459e984cdc..8589fc652340a 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts @@ -5,18 +5,15 @@ describe('GenericFunctions - searchUsers', () => { const mockGoogleApiRequestAllItems = jest.fn(); const mockContext = { - helpers: { - requestOAuth2: mockGoogleApiRequestAllItems, - }, + googleApiRequestAllItems: mockGoogleApiRequestAllItems, } as unknown as ILoadOptionsFunctions; beforeEach(() => { mockGoogleApiRequestAllItems.mockClear(); }); - //TODO - this test need to be fixed it('should return a list of users when API responds with users', async () => { - mockGoogleApiRequestAllItems.mockResolvedValue([ + mockGoogleApiRequestAllItems.mockResolvedValueOnce([ { id: '1', name: { fullName: 'John Doe' }, @@ -37,35 +34,16 @@ describe('GenericFunctions - searchUsers', () => { { name: 'Jane Smith', value: '2' }, ], }); - }); - - it('should return an empty array when API responds with no users', async () => { - mockGoogleApiRequestAllItems.mockResolvedValue([]); - - const result = await searchUsers.call(mockContext); - - expect(result).toEqual({ results: [] }); - }); - - it('should handle missing fields gracefully', async () => { - mockGoogleApiRequestAllItems.mockResolvedValue([ - { primaryEmail: 'john.doe@example.com', id: '1' }, - { name: { fullName: 'Jane Smith' }, id: '2' }, + expect(mockGoogleApiRequestAllItems).toHaveBeenCalledWith( + 'users', + 'GET', + '/directory/v1/users', {}, - ]); - - const result = await searchUsers.call(mockContext); - - expect(result).toEqual({ - results: [ - { name: 'john.doe@example.com', value: '1' }, - { name: 'Jane Smith', value: '2' }, - { name: 'Unnamed User', value: undefined }, - ], - }); + { customer: 'my_customer' }, + ); }); - it('should warn and return an empty array when no users are found', async () => { + it('should return an empty array when API responds with no users', async () => { mockGoogleApiRequestAllItems.mockResolvedValue([]); const result = await searchUsers.call(mockContext); From 6b072749726a356521a3973245adf94e1805cd01 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Wed, 15 Jan 2025 20:36:37 +0200 Subject: [PATCH 18/39] add custom fields inuser update operation --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 16 +++--- .../Google/GSuiteAdmin/UserDescription.ts | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 1790b8e4b8f60..c077ea317cd27 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -107,11 +107,14 @@ export class GSuiteAdmin implements INodeType { }, async getSchemaFields(this: ILoadOptionsFunctions): Promise { const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject; - console.log('additionalFields', JSON.stringify(additionalFields)); + const updateFields = this.getNodeParameter('updateFields', {}) as IDataObject; - if (additionalFields.customFields) { - } else { - console.warn('No Custom Fields found in additionalFields.'); + const fieldsContainer = { ...additionalFields, ...updateFields }; + + const customFields = (fieldsContainer.customFields as IDataObject) + .fieldValues as IDataObject[]; + const schemaName = customFields?.[0]?.schemaName as string; + if (!schemaName) { return [ { name: 'Invalid Schema: Missing schemaName.', @@ -119,11 +122,6 @@ export class GSuiteAdmin implements INodeType { }, ]; } - const customFields = (additionalFields.customFields as IDataObject) - .fieldValues as IDataObject[]; - console.log('customFields', customFields); - const schemaName = customFields?.[0]?.schemaName as string; - console.log('Schema Name:', schemaName); try { const schema = await googleApiRequest.call( diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index 9d645d6680f35..5004145762b0d 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -1500,6 +1500,57 @@ export const userFields: INodeProperties[] = [ }, ], }, + { + displayName: 'Custom Fields', + name: 'customFields', + placeholder: 'Add or Edit Custom Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + description: 'Allows editing and adding of custom fields', + options: [ + { + name: 'fieldValues', + displayName: 'Field', + values: [ + { + displayName: 'Schema Name or ID', + name: 'schemaName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSchemas', + }, + default: '', + description: + 'Select the schema to use for custom fields. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Field Name or ID', + name: 'fieldName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSchemaFields', + loadOptionsDependsOn: ['customFields.fieldValues.schema.schemaName'], + }, + default: '', + required: true, + description: + 'Select the field from the selected schema. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + required: true, + description: 'Provide a value for the selected field', + }, + ], + }, + ], + }, ], }, ]; From acd69f3e671faf2e9d371e20df6957193f64ea12 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Wed, 15 Jan 2025 21:08:04 +0200 Subject: [PATCH 19/39] fix searchGroups and SearchUsers tests --- .../GSuiteAdmin/test/SearchGroups.test.ts | 42 +++++++++++------ .../GSuiteAdmin/test/SearchUsers.test.ts | 45 ++++++++++--------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts index 1e42e6c89900f..5f4aaf4a6796c 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchGroups.test.ts @@ -1,21 +1,19 @@ import type { ILoadOptionsFunctions } from 'n8n-workflow'; -import { searchGroups } from '../GenericFunctions'; -describe('GenericFunctions - searchGroups', () => { - const mockGoogleApiRequestAllItems = jest.fn(); +import { googleApiRequestAllItems } from '../GenericFunctions'; +import { searchGroups } from '../SearchFunctions'; + +jest.mock('../GenericFunctions'); - const mockContext = { - helpers: { - requestOAuth2: mockGoogleApiRequestAllItems, - }, - } as unknown as ILoadOptionsFunctions; +describe('GenericFunctions - searchGroups', () => { + const mockContext = {} as unknown as ILoadOptionsFunctions; beforeEach(() => { - mockGoogleApiRequestAllItems.mockClear(); + (googleApiRequestAllItems as jest.Mock).mockClear(); }); - //TODO - this test not works - it('should return a list of groups when API responds with groups', async () => { - mockGoogleApiRequestAllItems.mockResolvedValue([ + + it('should return a list of groups when googleApiRequestAllItems returns groups', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ { kind: 'admin#directory#group', id: '01302m922pmp3e4', @@ -40,13 +38,29 @@ describe('GenericFunctions - searchGroups', () => { { name: 'NewOness', value: '01x0gk373c9z46j' }, ], }); + expect(googleApiRequestAllItems).toHaveBeenCalledTimes(1); + expect(googleApiRequestAllItems).toHaveBeenCalledWith( + 'groups', + 'GET', + '/directory/v1/groups', + {}, + { customer: 'my_customer' }, + ); }); - it('should return an empty array when API responds with no groups', async () => { - mockGoogleApiRequestAllItems.mockResolvedValue([]); + it('should return an empty array when googleApiRequestAllItems returns no groups', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); const result = await searchGroups.call(mockContext); expect(result).toEqual({ results: [] }); + expect(googleApiRequestAllItems).toHaveBeenCalledTimes(1); + expect(googleApiRequestAllItems).toHaveBeenCalledWith( + 'groups', + 'GET', + '/directory/v1/groups', + {}, + { customer: 'my_customer' }, + ); }); }); diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts index 8589fc652340a..56153674c9b18 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchUsers.test.ts @@ -1,29 +1,21 @@ import type { ILoadOptionsFunctions } from 'n8n-workflow'; -import { searchUsers } from '../GenericFunctions'; -describe('GenericFunctions - searchUsers', () => { - const mockGoogleApiRequestAllItems = jest.fn(); +import { googleApiRequestAllItems } from '../GenericFunctions'; +import { searchUsers } from '../SearchFunctions'; + +jest.mock('../GenericFunctions'); - const mockContext = { - googleApiRequestAllItems: mockGoogleApiRequestAllItems, - } as unknown as ILoadOptionsFunctions; +describe('GenericFunctions - searchUsers', () => { + const mockContext = {} as unknown as ILoadOptionsFunctions; beforeEach(() => { - mockGoogleApiRequestAllItems.mockClear(); + (googleApiRequestAllItems as jest.Mock).mockClear(); }); - it('should return a list of users when API responds with users', async () => { - mockGoogleApiRequestAllItems.mockResolvedValueOnce([ - { - id: '1', - name: { fullName: 'John Doe' }, - primaryEmail: 'john.doe@example.com', - }, - { - id: '2', - name: { fullName: 'Jane Smith' }, - primaryEmail: 'jane.smith@example.com', - }, + it('should return a list of users when googleApiRequestAllItems returns users', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([ + { id: '1', name: { fullName: 'John Doe' } }, + { id: '2', name: { fullName: 'Jane Smith' } }, ]); const result = await searchUsers.call(mockContext); @@ -34,7 +26,8 @@ describe('GenericFunctions - searchUsers', () => { { name: 'Jane Smith', value: '2' }, ], }); - expect(mockGoogleApiRequestAllItems).toHaveBeenCalledWith( + expect(googleApiRequestAllItems).toHaveBeenCalledTimes(1); + expect(googleApiRequestAllItems).toHaveBeenCalledWith( 'users', 'GET', '/directory/v1/users', @@ -43,11 +36,19 @@ describe('GenericFunctions - searchUsers', () => { ); }); - it('should return an empty array when API responds with no users', async () => { - mockGoogleApiRequestAllItems.mockResolvedValue([]); + it('should return an empty array when googleApiRequestAllItems returns no users', async () => { + (googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]); const result = await searchUsers.call(mockContext); expect(result).toEqual({ results: [] }); + expect(googleApiRequestAllItems).toHaveBeenCalledTimes(1); + expect(googleApiRequestAllItems).toHaveBeenCalledWith( + 'users', + 'GET', + '/directory/v1/users', + {}, + { customer: 'my_customer' }, + ); }); }); From a2ef494313d2ba1ccfba83eea04d179f69882387 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Wed, 15 Jan 2025 21:37:15 +0200 Subject: [PATCH 20/39] separate search functions in new file --- .../Google/GSuiteAdmin/GenericFunctions.ts | 65 ----------------- .../Google/GSuiteAdmin/SearchFunctions.ts | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 65 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts index 89ac4dbbccc30..7f30fe6812e29 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GenericFunctions.ts @@ -5,8 +5,6 @@ import type { JsonObject, IHttpRequestMethods, IRequestOptions, - INodeListSearchResult, - INodeListSearchItems, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; @@ -66,66 +64,3 @@ export async function googleApiRequestAllItems( return returnData; } - -/* listSearch methods */ -export async function searchUsers(this: ILoadOptionsFunctions): Promise { - const qs: IDataObject = { - customer: 'my_customer', - }; - - // Perform the API request to list all users - const responseData = await googleApiRequestAllItems.call( - this, - 'users', - 'GET', - '/directory/v1/users', - {}, - qs, - ); - - // Handle cases where no users are found - if (!responseData || responseData.length === 0) { - return { results: [] }; - } - - //Map the API response - const results: INodeListSearchItems[] = responseData.map( - (user: { name?: { fullName?: string }; id?: string }) => ({ - name: user.name?.fullName, - value: user.id, - }), - ); - - return { results }; -} - -export async function searchGroups(this: ILoadOptionsFunctions): Promise { - const qs: IDataObject = { - customer: 'my_customer', - }; - - // Perform the API request to list all groups - const responseData = await googleApiRequestAllItems.call( - this, - 'groups', - 'GET', - '/directory/v1/groups', - {}, - qs, - ); - - // Handle cases where no groups are found - if (!responseData || responseData.length === 0) { - return { results: [] }; - } - - //Map the API response - const results: INodeListSearchItems[] = responseData.map( - (group: { name?: string; email?: string; id?: string }) => ({ - name: group.name || group.email || 'Unnamed Group', - value: group.id, - }), - ); - - return { results }; -} diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts new file mode 100644 index 0000000000000..3674ea4412c7d --- /dev/null +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts @@ -0,0 +1,70 @@ +import type { + ILoadOptionsFunctions, + IDataObject, + INodeListSearchResult, + INodeListSearchItems, +} from 'n8n-workflow'; + +import { googleApiRequestAllItems } from './GenericFunctions'; + +export async function searchUsers(this: ILoadOptionsFunctions): Promise { + const qs: IDataObject = { + customer: 'my_customer', + }; + + // Perform the API request to list all users + const responseData = await googleApiRequestAllItems.call( + this, + 'users', + 'GET', + '/directory/v1/users', + {}, + qs, + ); + + // Handle cases where no users are found + if (!responseData || responseData.length === 0) { + return { results: [] }; + } + + //Map the API response + const results: INodeListSearchItems[] = responseData.map( + (user: { name?: { fullName?: string }; id?: string }) => ({ + name: user.name?.fullName, + value: user.id, + }), + ); + + return { results }; +} + +export async function searchGroups(this: ILoadOptionsFunctions): Promise { + const qs: IDataObject = { + customer: 'my_customer', + }; + + // Perform the API request to list all groups + const responseData = await googleApiRequestAllItems.call( + this, + 'groups', + 'GET', + '/directory/v1/groups', + {}, + qs, + ); + + // Handle cases where no groups are found + if (!responseData || responseData.length === 0) { + return { results: [] }; + } + + //Map the API response + const results: INodeListSearchItems[] = responseData.map( + (group: { name?: string; email?: string; id?: string }) => ({ + name: group.name || group.email || 'Unnamed Group', + value: group.id, + }), + ); + + return { results }; +} From 7c7e9740873689ed8de650eb6685b34420508425 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Wed, 15 Jan 2025 21:43:35 +0200 Subject: [PATCH 21/39] fix custom fields --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 22 +++++++------------ .../Google/GSuiteAdmin/UserDescription.ts | 5 ++++- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index c077ea317cd27..38062a415d8dd 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -10,13 +10,9 @@ import type { import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { deviceFields, deviceOperations } from './DeviceDescription'; -import { - googleApiRequest, - googleApiRequestAllItems, - searchGroups, - searchUsers, -} from './GenericFunctions'; +import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; import { groupFields, groupOperations } from './GroupDescripion'; +import { searchGroups, searchUsers } from './SearchFunctions'; import { userFields, userOperations } from './UserDescription'; export class GSuiteAdmin implements INodeType { @@ -107,14 +103,9 @@ export class GSuiteAdmin implements INodeType { }, async getSchemaFields(this: ILoadOptionsFunctions): Promise { const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject; - const updateFields = this.getNodeParameter('updateFields', {}) as IDataObject; - - const fieldsContainer = { ...additionalFields, ...updateFields }; - - const customFields = (fieldsContainer.customFields as IDataObject) - .fieldValues as IDataObject[]; - const schemaName = customFields?.[0]?.schemaName as string; - if (!schemaName) { + this.getExecutionId(); + if (additionalFields.customFields) { + } else { return [ { name: 'Invalid Schema: Missing schemaName.', @@ -122,6 +113,9 @@ export class GSuiteAdmin implements INodeType { }, ]; } + const customFields = (additionalFields.customFields as IDataObject) + .fieldValues as IDataObject[]; + const schemaName = customFields?.[0]?.schemaName as string; try { const schema = await googleApiRequest.call( diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index 5004145762b0d..0af2b8a06abdf 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -547,7 +547,10 @@ export const userFields: INodeProperties[] = [ name: 'fieldName', type: 'options', typeOptions: { - loadOptionsDependsOn: ['schemaName.value'], + loadOptionsDependsOn: [ + 'additionalFields.test', + 'additionalFields.customFields.fieldValues[0].schemaName', + ], loadOptionsMethod: 'getSchemaFields', }, default: '', From 9b3609f8d8d4ae43e0226b7921ffdacc255cd2a4 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Thu, 16 Jan 2025 01:05:40 +0200 Subject: [PATCH 22/39] remove unused variable --- .../nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 38062a415d8dd..8ef13b150cb49 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -655,10 +655,6 @@ export class GSuiteAdmin implements INodeType { if (operation === 'removeFromGroup') { const groupId = (this.getNodeParameter('groupId', i) as IDataObject).value; const userId = (this.getNodeParameter('userId', i) as IDataObject).value; - const body: IDataObject = { - email: userId, - role: 'MEMBER', - }; await googleApiRequest.call( this, From 21f725e5ad82a38fbce39a41c2632e95d76da4b0 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Thu, 16 Jan 2025 03:24:28 +0200 Subject: [PATCH 23/39] update custom fields --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 72 +++++++++---------- .../Google/GSuiteAdmin/UserDescription.ts | 34 ++++----- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 8ef13b150cb49..918185adca37d 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -101,42 +101,42 @@ export class GSuiteAdmin implements INodeType { value: schema.schemaName, })); }, - async getSchemaFields(this: ILoadOptionsFunctions): Promise { - const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject; - this.getExecutionId(); - if (additionalFields.customFields) { - } else { - return [ - { - name: 'Invalid Schema: Missing schemaName.', - value: '', - }, - ]; - } - const customFields = (additionalFields.customFields as IDataObject) - .fieldValues as IDataObject[]; - const schemaName = customFields?.[0]?.schemaName as string; - - try { - const schema = await googleApiRequest.call( - this, - 'GET', - `/directory/v1/customer/my_customer/schemas/${schemaName}`, - ); - - if (schema.fields && Array.isArray(schema.fields)) { - return schema.fields.map((field: { fieldName: string; displayName: string }) => ({ - name: field.displayName || field.fieldName, - value: field.fieldName, - })); - } else { - } - } catch (error) { - console.error('Error fetching schema fields:', error); - } - - return []; - }, + // async getSchemaFields(this: ILoadOptionsFunctions): Promise { + // const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject; + // this.getExecutionId(); + // if (additionalFields.customFields) { + // } else { + // return [ + // { + // name: 'Invalid Schema: Missing schemaName.', + // value: '', + // }, + // ]; + // } + // const customFields = (additionalFields.customFields as IDataObject) + // .fieldValues as IDataObject[]; + // const schemaName = customFields?.[0]?.schemaName as string; + + // try { + // const schema = await googleApiRequest.call( + // this, + // 'GET', + // `/directory/v1/customer/my_customer/schemas/${schemaName}`, + // ); + + // if (schema.fields && Array.isArray(schema.fields)) { + // return schema.fields.map((field: { fieldName: string; displayName: string }) => ({ + // name: field.displayName || field.fieldName, + // value: field.fieldName, + // })); + // } else { + // } + // } catch (error) { + // console.error('Error fetching schema fields:', error); + // } + + // return []; + // }, async getOrgUnits(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const orgUnits = await googleApiRequest.call( diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index 0af2b8a06abdf..dcc5e7ddb16c7 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -545,18 +545,18 @@ export const userFields: INodeProperties[] = [ { displayName: 'Field Name or ID', name: 'fieldName', - type: 'options', - typeOptions: { - loadOptionsDependsOn: [ - 'additionalFields.test', - 'additionalFields.customFields.fieldValues[0].schemaName', - ], - loadOptionsMethod: 'getSchemaFields', - }, + type: 'string', + // type: 'options', + // typeOptions: { + // loadOptionsDependsOn: [ + // 'additionalFields.test', + // 'additionalFields.customFields.fieldValues[0].schemaName', + // ], + // loadOptionsMethod: 'getSchemaFields', + // }, default: '', required: true, - description: - 'Select the field from the selected schema. Choose from the list, or specify an ID using an expression.', + description: 'Enter a field name from the selected schema', }, { displayName: 'Value', @@ -1532,15 +1532,15 @@ export const userFields: INodeProperties[] = [ { displayName: 'Field Name or ID', name: 'fieldName', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getSchemaFields', - loadOptionsDependsOn: ['customFields.fieldValues.schema.schemaName'], - }, + type: 'string', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getSchemaFields', + // loadOptionsDependsOn: ['customFields.fieldValues.schema.schemaName'], + // }, default: '', required: true, - description: - 'Select the field from the selected schema. Choose from the list, or specify an ID using an expression.', + description: 'Enter a field name from the selected schema', }, { displayName: 'Value', From fbe7da628a864be701811dbea03dbc45b3736415 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Thu, 16 Jan 2025 12:29:00 +0200 Subject: [PATCH 24/39] format --- .../Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts index 0554412827c70..9b10b2624556d 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/GoogleApiRequestAllItems.test.ts @@ -1,4 +1,5 @@ import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow'; + import { googleApiRequestAllItems } from '../GenericFunctions'; describe('googleApiRequestAllItems', () => { From 98c980f2d26bff5a6f990740e442693c55be1b22 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Fri, 17 Jan 2025 13:42:26 +0200 Subject: [PATCH 25/39] format --- packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 772816aafaa4c..918185adca37d 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -13,7 +13,6 @@ import { deviceFields, deviceOperations } from './DeviceDescription'; import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; import { groupFields, groupOperations } from './GroupDescripion'; import { searchGroups, searchUsers } from './SearchFunctions'; - import { userFields, userOperations } from './UserDescription'; export class GSuiteAdmin implements INodeType { From d5d2a57d9bcabef9bfd4e47ebcf10b9d79e38206 Mon Sep 17 00:00:00 2001 From: Giulio Andreini Date: Tue, 4 Feb 2025 08:52:55 +0100 Subject: [PATCH 26/39] Minor copy tweaks. --- .../Google/GSuiteAdmin/DeviceDescription.ts | 4 ++-- .../Google/GSuiteAdmin/GroupDescripion.ts | 11 +++++------ .../Google/GSuiteAdmin/UserDescription.ts | 18 ++++++++---------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index 8979c28af8718..3ffebf0b5e251 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -63,7 +63,7 @@ export const deviceFields: INodeProperties[] = [ /* device:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'Return All?', + displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, @@ -145,7 +145,7 @@ export const deviceFields: INodeProperties[] = [ 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression.', }, { - displayName: 'Include Children?', + displayName: 'Include Children', name: 'includeChildOrgunits', type: 'boolean', default: false, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts index 0d19366ac75e7..4174c55cc92a5 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GroupDescripion.ts @@ -135,7 +135,6 @@ export const groupFields: INodeProperties[] = [ displayName: 'By ID', name: 'GroupId', type: 'string', - hint: 'Enter the group id', placeholder: 'e.g. 0123kx3o1habcdf', }, ], @@ -174,7 +173,6 @@ export const groupFields: INodeProperties[] = [ displayName: 'By ID', name: 'groupId', type: 'string', - hint: 'Enter the group id', placeholder: 'e.g. 0123kx3o1habcdf', }, ], @@ -248,7 +246,7 @@ export const groupFields: INodeProperties[] = [ placeholder: 'e.g. name:contact* email:contact*', default: '', description: - 'Query string to filter the results. Follow Google Admin SDK documentation .', + 'Query string to filter the results. Follow Google Admin SDK documentation. More info.', }, { displayName: 'User ID', @@ -286,7 +284,7 @@ export const groupFields: INodeProperties[] = [ value: 'email', }, ], - default: '', + default: 'email', description: 'Field to sort the results by', }, { @@ -340,8 +338,6 @@ export const groupFields: INodeProperties[] = [ displayName: 'By ID', name: 'groupId', type: 'string', - hint: 'Enter the group id', - placeholder: 'e.g. 0123kx3o1habcdf', }, ], @@ -366,6 +362,9 @@ export const groupFields: INodeProperties[] = [ name: 'description', type: 'string', default: '', + typeOptions: { + rows: 2, + }, description: 'An extended description to help users determine the purpose of a group. For example, you can include information about who should join the group, the types of messages to send to the group, links to FAQs about the group, or related groups.', }, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index dcc5e7ddb16c7..79560381e0313 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -142,7 +142,6 @@ export const userFields: INodeProperties[] = [ displayName: 'By ID', name: 'groupId', type: 'string', - hint: 'Enter the group id', placeholder: 'e.g. 0123kx3o1habcdf', }, ], @@ -576,7 +575,7 @@ export const userFields: INodeProperties[] = [ /* user:delete */ /* -------------------------------------------------------------------------- */ { - displayName: 'User ID', + displayName: 'User', name: 'userId', default: { mode: 'list', @@ -629,7 +628,7 @@ export const userFields: INodeProperties[] = [ /* user:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'User ID', + displayName: 'User', name: 'userId', type: 'resourceLocator', required: true, @@ -733,7 +732,7 @@ export const userFields: INodeProperties[] = [ description: 'Fields to include in the response when "Select Included Fields" is chosen', }, { - displayName: 'Projection', + displayName: 'Custom Fields', name: 'projection', type: 'options', required: true, @@ -780,7 +779,7 @@ export const userFields: INodeProperties[] = [ }, default: [], description: - 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression.', + 'A comma-separated list of schema names. All fields from these schemas are fetched. Choose from the list, or specify IDs using an expression.', }, /* -------------------------------------------------------------------------- */ /* user:getAll */ @@ -871,7 +870,7 @@ export const userFields: INodeProperties[] = [ description: 'Fields to include in the response when "Select Included Fields" is chosen', }, { - displayName: 'Projection', + displayName: 'Custom Fields', name: 'projection', type: 'options', required: true, @@ -918,7 +917,7 @@ export const userFields: INodeProperties[] = [ }, default: [], description: - 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression.', + 'A comma-separated list of schema names. All fields from these schemas are fetched. Choose from the list, or specify IDs using an expression.', }, { displayName: 'Filter', @@ -954,7 +953,7 @@ export const userFields: INodeProperties[] = [ placeholder: 'e.g. name:contact* email:contact*', default: '', description: - 'Query string to filter the results. Follow Google Admin SDK documentation .', + 'Query string to filter the results. Follow Google Admin SDK documentation. More info.', }, { displayName: 'Show Deleted', @@ -1109,7 +1108,6 @@ export const userFields: INodeProperties[] = [ displayName: 'By ID', name: 'groupId', type: 'string', - hint: 'Enter the group id', placeholder: 'e.g. 0123kx3o1habcdf', }, ], @@ -1118,7 +1116,7 @@ export const userFields: INodeProperties[] = [ /* user:update */ /* -------------------------------------------------------------------------- */ { - displayName: 'User ID', + displayName: 'User', name: 'userId', type: 'resourceLocator', required: true, From 6bb6d494a41ca3294769b2307a68d2d5515306fc Mon Sep 17 00:00:00 2001 From: Stanimira Rikova Date: Wed, 5 Feb 2025 13:40:20 +0200 Subject: [PATCH 27/39] throw an error if username filed is empty --- .../nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 918185adca37d..54d1e2978ac13 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -406,6 +406,13 @@ export class GSuiteAdmin implements INodeType { const additionalFields = this.getNodeParameter('additionalFields', i); + if (!username) { + throw new NodeOperationError( + this.getNode(), + 'The parameter ‘Username’ is empty. Please fill in the ‘Username’ parameter to create the user.', + { itemIndex: i }, + ); + } const body: IDataObject = { name: { familyName: lastName, @@ -880,6 +887,9 @@ export class GSuiteAdmin implements INodeType { returnData.push(...executionData); } catch (error) { + if (error instanceof NodeOperationError) { + throw error; + } if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ From 3625f4ef5d0ed84d2e78efc969d29d9c1ae157cf Mon Sep 17 00:00:00 2001 From: Stanimira Rikova Date: Wed, 5 Feb 2025 17:10:16 +0200 Subject: [PATCH 28/39] fix roles as multi-options component --- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 49 ++- .../Google/GSuiteAdmin/UserDescription.ts | 284 +++++++----------- 2 files changed, 134 insertions(+), 199 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 54d1e2978ac13..20d770ad2affe 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -437,20 +437,19 @@ export class GSuiteAdmin implements INodeType { } if (additionalFields.roles) { - const roles = (additionalFields.roles as IDataObject).rolesValues as IDataObject; - + const roles = additionalFields.roles as string[]; body.roles = { - superAdmin: Boolean(roles.superAdmin), - groupsAdmin: Boolean(roles.groupsAdmin), - groupsReader: Boolean(roles.groupsReader), - groupsEditor: Boolean(roles.groupsEditor), - userManagement: Boolean(roles.userManagement), - helpDeskAdmin: Boolean(roles.helpDeskAdmin), - servicesAdmin: Boolean(roles.servicesAdmin), - inventoryReportingAdmin: Boolean(roles.inventoryReportingAdmin), - storageAdmin: Boolean(roles.storageAdmin), - directorySyncAdmin: Boolean(roles.directorySyncAdmin), - mobileAdmin: Boolean(roles.mobileAdmin), + superAdmin: roles.includes('superAdmin'), + groupsAdmin: roles.includes('groupsAdmin'), + groupsReader: roles.includes('groupsReader'), + groupsEditor: roles.includes('groupsEditor'), + userManagement: roles.includes('userManagement'), + helpDeskAdmin: roles.includes('helpDeskAdmin'), + servicesAdmin: roles.includes('servicesAdmin'), + inventoryReportingAdmin: roles.includes('inventoryReportingAdmin'), + storageAdmin: roles.includes('storageAdmin'), + directorySyncAdmin: roles.includes('directorySyncAdmin'), + mobileAdmin: roles.includes('mobileAdmin'), }; } @@ -725,20 +724,20 @@ export class GSuiteAdmin implements INodeType { } if (updateFields.roles) { - const roles = (updateFields.roles as IDataObject).rolesValues as IDataObject; + const roles = updateFields.roles as string[]; body.roles = { - superAdmin: Boolean(roles.superAdmin), - groupsAdmin: Boolean(roles.groupsAdmin), - groupsReader: Boolean(roles.groupsReader), - groupsEditor: Boolean(roles.groupsEditor), - userManagement: Boolean(roles.userManagement), - helpDeskAdmin: Boolean(roles.helpDeskAdmin), - servicesAdmin: Boolean(roles.servicesAdmin), - inventoryReportingAdmin: Boolean(roles.inventoryReportingAdmin), - storageAdmin: Boolean(roles.storageAdmin), - directorySyncAdmin: Boolean(roles.directorySyncAdmin), - mobileAdmin: Boolean(roles.mobileAdmin), + superAdmin: roles.includes('superAdmin'), + groupsAdmin: roles.includes('groupsAdmin'), + groupsReader: roles.includes('groupsReader'), + groupsEditor: roles.includes('groupsEditor'), + userManagement: roles.includes('userManagement'), + helpDeskAdmin: roles.includes('helpDeskAdmin'), + servicesAdmin: roles.includes('servicesAdmin'), + inventoryReportingAdmin: roles.includes('inventoryReportingAdmin'), + storageAdmin: roles.includes('storageAdmin'), + directorySyncAdmin: roles.includes('directorySyncAdmin'), + mobileAdmin: roles.includes('mobileAdmin'), }; } diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts index 79560381e0313..5386a6f652536 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/UserDescription.ts @@ -422,96 +422,64 @@ export const userFields: INodeProperties[] = [ { displayName: 'Roles', name: 'roles', - type: 'fixedCollection', - placeholder: 'Assign Roles', - typeOptions: { - multipleValues: false, - }, - default: {}, + type: 'multiOptions', + default: [], description: 'Select the roles you want to assign to the user', options: [ { - name: 'rolesValues', - displayName: 'Roles', - values: [ - { - displayName: 'Super Admin', - name: 'superAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Super Admin role', - }, - { - displayName: 'Groups Admin', - name: 'groupsAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Groups Admin role', - }, - { - displayName: 'Groups Reader', - name: 'groupsReader', - type: 'boolean', - default: false, - description: 'Whether Assign Groups Reader role', - }, - { - displayName: 'Groups Editor', - name: 'groupsEditor', - type: 'boolean', - default: false, - description: 'Whether Assign Groups Editor role', - }, - { - displayName: 'User Management', - name: 'userManagement', - type: 'boolean', - default: false, - description: 'Whether Assign User Management role', - }, - { - displayName: 'Help Desk Admin', - name: 'helpDeskAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Help Desk Admin role', - }, - { - displayName: 'Services Admin', - name: 'servicesAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Services Admin role', - }, - { - displayName: 'Inventory Reporting Admin', - name: 'inventoryReportingAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Inventory Reporting Admin role', - }, - { - displayName: 'Storage Admin', - name: 'storageAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Storage Admin role', - }, - { - displayName: 'Directory Sync Admin', - name: 'directorySyncAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Directory Sync Admin role', - }, - { - displayName: 'Mobile Admin', - name: 'mobileAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Mobile Admin role', - }, - ], + name: 'Directory Sync Admin', + value: 'directorySyncAdmin', + description: 'Whether to assign the Directory Sync Admin role', + }, + { + name: 'Groups Admin', + value: 'groupsAdmin', + description: 'Whether to assign the Groups Admin role', + }, + { + name: 'Groups Editor', + value: 'groupsEditor', + description: 'Whether to assign the Groups Editor role', + }, + { + name: 'Groups Reader', + value: 'groupsReader', + description: 'Whether to assign the Groups Reader role', + }, + { + name: 'Help Desk Admin', + value: 'helpDeskAdmin', + description: 'Whether to assign the Help Desk Admin role', + }, + { + name: 'Inventory Reporting Admin', + value: 'inventoryReportingAdmin', + description: 'Whether to assign the Inventory Reporting Admin role', + }, + { + name: 'Mobile Admin', + value: 'mobileAdmin', + description: 'Whether to assign the Mobile Admin role', + }, + { + name: 'Services Admin', + value: 'servicesAdmin', + description: 'Whether to assign the Services Admin role', + }, + { + name: 'Storage Admin', + value: 'storageAdmin', + description: 'Whether to assign the Storage Admin role', + }, + { + name: 'Super Admin', + value: 'superAdmin', + description: 'Whether to assign the Super Admin role', + }, + { + name: 'User Management', + value: 'userManagement', + description: 'Whether to assign the User Management role', }, ], }, @@ -1408,96 +1376,64 @@ export const userFields: INodeProperties[] = [ { displayName: 'Roles', name: 'roles', - type: 'fixedCollection', - placeholder: 'Assign Roles', - typeOptions: { - multipleValues: false, - }, - default: {}, + type: 'multiOptions', + default: [], description: 'Select the roles you want to assign to the user', options: [ { - name: 'rolesValues', - displayName: 'Roles', - values: [ - { - displayName: 'Super Admin', - name: 'superAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Super Admin role', - }, - { - displayName: 'Groups Admin', - name: 'groupsAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Groups Admin role', - }, - { - displayName: 'Groups Reader', - name: 'groupsReader', - type: 'boolean', - default: false, - description: 'Whether Assign Groups Reader role', - }, - { - displayName: 'Groups Editor', - name: 'groupsEditor', - type: 'boolean', - default: false, - description: 'Whether Assign Groups Editor role', - }, - { - displayName: 'User Management', - name: 'userManagement', - type: 'boolean', - default: false, - description: 'Whether Assign User Management role', - }, - { - displayName: 'Help Desk Admin', - name: 'helpDeskAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Help Desk Admin role', - }, - { - displayName: 'Services Admin', - name: 'servicesAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Services Admin role', - }, - { - displayName: 'Inventory Reporting Admin', - name: 'inventoryReportingAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Inventory Reporting Admin role', - }, - { - displayName: 'Storage Admin', - name: 'storageAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Storage Admin role', - }, - { - displayName: 'Directory Sync Admin', - name: 'directorySyncAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Directory Sync Admin role', - }, - { - displayName: 'Mobile Admin', - name: 'mobileAdmin', - type: 'boolean', - default: false, - description: 'Whether Assign Mobile Admin role', - }, - ], + name: 'Directory Sync Admin', + value: 'directorySyncAdmin', + description: 'Whether Assign Directory Sync Admin role', + }, + { + name: 'Groups Admin', + value: 'groupsAdmin', + description: 'Whether Assign Groups Admin role', + }, + { + name: 'Groups Editor', + value: 'groupsEditor', + description: 'Whether Assign Groups Editor role', + }, + { + name: 'Groups Reader', + value: 'groupsReader', + description: 'Whether Assign Groups Reader role', + }, + { + name: 'Help Desk Admin', + value: 'helpDeskAdmin', + description: 'Whether Assign Help Desk Admin role', + }, + { + name: 'Inventory Reporting Admin', + value: 'inventoryReportingAdmin', + description: 'Whether Assign Inventory Reporting Admin role', + }, + { + name: 'Mobile Admin', + value: 'mobileAdmin', + description: 'Whether Assign Mobile Admin role', + }, + { + name: 'Services Admin', + value: 'servicesAdmin', + description: 'Whether Assign Services Admin role', + }, + { + name: 'Storage Admin', + value: 'storageAdmin', + description: 'Whether Assign Storage Admin role', + }, + { + name: 'Super Admin', + value: 'superAdmin', + description: 'Whether Assign Super Admin role', + }, + { + name: 'User Management', + value: 'userManagement', + description: 'Whether Assign User Management role', }, ], }, From ad5f3c041180f91fac974535ccc2ab6e3d80ee0c Mon Sep 17 00:00:00 2001 From: Stanimira Rikova Date: Wed, 5 Feb 2025 23:38:16 +0200 Subject: [PATCH 29/39] remove projection --- .../nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts | 2 +- .../nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index 3ffebf0b5e251..3c4f12a6cb961 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -113,7 +113,7 @@ export const deviceFields: INodeProperties[] = [ ], displayOptions: { show: { - operation: ['get', 'getAll', 'update'], + operation: ['get', 'getAll'], resource: ['device'], }, }, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 20d770ad2affe..d34f0d3dba831 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -845,7 +845,6 @@ export class GSuiteAdmin implements INodeType { if (operation === 'update') { const deviceId = this.getNodeParameter('deviceId', i) as string; - const projection = this.getNodeParameter('projection', 1); const updateOptions = this.getNodeParameter('updateOptions', 1); // Validate deviceId @@ -860,7 +859,7 @@ export class GSuiteAdmin implements INodeType { responseData = await googleApiRequest.call( this, 'PUT', - `/directory/v1/customer/my_customer/devices/chromeos/${deviceId}?projection=${projection}`, + `/directory/v1/customer/my_customer/devices/chromeos/${deviceId}`, qs, ); } From 8945b57f2fc9e1c6e799566d86b2b8a3c585b0be Mon Sep 17 00:00:00 2001 From: Stanimira Rikova Date: Wed, 5 Feb 2025 23:41:34 +0200 Subject: [PATCH 30/39] fix field names in change status --- .../nodes/Google/GSuiteAdmin/DeviceDescription.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index 3c4f12a6cb961..9e4d67af9422e 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -283,19 +283,19 @@ export const deviceFields: INodeProperties[] = [ /* device:changeStatus */ /* -------------------------------------------------------------------------- */ { - displayName: 'Action', + displayName: 'Status', name: 'action', type: 'options', required: true, options: [ { - name: 'Enable', + name: 'Enabled', value: 'reenable', description: 'Re-enable a disabled chromebook', action: 'Enable a device', }, { - name: 'Disable', + name: 'Disabled', value: 'disable', description: 'Disable a chromebook', action: 'Disable a device', From e4010fc0f88669af701701f36e6f15ddadecc9a8 Mon Sep 17 00:00:00 2001 From: Stanimira Rikova Date: Thu, 6 Feb 2025 00:04:40 +0200 Subject: [PATCH 31/39] use a Resource Locator for device id field --- .../Google/GSuiteAdmin/DeviceDescription.ts | 28 ++++++++++++++--- .../Google/GSuiteAdmin/SearchFunctions.ts | 31 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index 9e4d67af9422e..cd11db6a1039c 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -46,9 +46,9 @@ export const deviceFields: INodeProperties[] = [ /* device:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Device ID', + displayName: 'Device', name: 'deviceId', - type: 'string', + type: 'resourceLocator', required: true, displayOptions: { show: { @@ -56,8 +56,28 @@ export const deviceFields: INodeProperties[] = [ resource: ['device'], }, }, - default: '', - placeholder: 'e.g. 123e4567-e89b-12d3-a456-426614174000', + default: { + mode: 'list', + value: '', + }, + description: 'Select the device you want to retrieve', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchDevices', + }, + }, + { + displayName: 'By ID', + name: 'deviceId', + type: 'string', + hint: 'Enter the device id', + placeholder: 'e.g. 123e4567-e89b-12d3-a456-426614174000', + }, + ], }, /* -------------------------------------------------------------------------- */ /* device:getAll */ diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts index 3674ea4412c7d..02c23c84e6c10 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts @@ -68,3 +68,34 @@ export async function searchGroups(this: ILoadOptionsFunctions): Promise { + const qs: IDataObject = { + customerId: 'my_customer', + }; + + // Perform the API request to list all ChromeOS devices + const responseData = await googleApiRequestAllItems.call( + this, + 'chromeosdevices', + 'GET', + '/directory/v1/customer/my_customer/devices/chromeos', + {}, + qs, + ); + + // Handle cases where no devices are found + if (!responseData || responseData.length === 0) { + return { results: [] }; + } + + // Map the API response + const results: INodeListSearchItems[] = responseData.map( + (device: { deviceId?: string; serialNumber?: string }) => ({ + name: device.serialNumber || 'Unknown Device', + value: device.deviceId, + }), + ); + + return { results }; +} From ecc9362fbddfd9a19ef472fe0d34edeaea2b8664 Mon Sep 17 00:00:00 2001 From: Stanimira Rikova Date: Thu, 6 Feb 2025 00:32:35 +0200 Subject: [PATCH 32/39] =?UTF-8?q?rename=20Projection=20as=20=E2=80=9COutpu?= =?UTF-8?q?t=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nodes/Google/GSuiteAdmin/DeviceDescription.ts | 2 +- .../nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index cd11db6a1039c..9c678244615e6 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -115,7 +115,7 @@ export const deviceFields: INodeProperties[] = [ description: 'Max number of results to return', }, { - displayName: 'Projection', + displayName: 'Output', name: 'projection', type: 'options', required: true, diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index d34f0d3dba831..a0a550ea96cb8 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -783,7 +783,7 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/deviceId if (operation === 'get') { const deviceId = this.getNodeParameter('deviceId', i) as string; - const projection = this.getNodeParameter('projection', 1); + const output = this.getNodeParameter('projection', 1); // Validate deviceId if (!deviceId) { @@ -797,7 +797,7 @@ export class GSuiteAdmin implements INodeType { responseData = await googleApiRequest.call( this, 'GET', - `/directory/v1/customer/my_customer/devices/chromeos/${deviceId}?projection=${projection}`, + `/directory/v1/customer/my_customer/devices/chromeos/${deviceId}?projection=${output}`, {}, ); } @@ -805,11 +805,11 @@ export class GSuiteAdmin implements INodeType { //https://developers.google.com/admin-sdk/directory/reference/rest/v1/chromeosdevices/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i); - const projection = this.getNodeParameter('projection', 1); + const output = this.getNodeParameter('projection', 1); const options = this.getNodeParameter('options', 2); - qs.projection = projection; + qs.projection = output; Object.assign(qs, options); if (qs.customer === undefined) { From e9008d2999daf3148f20a4922cfea248e10d5543 Mon Sep 17 00:00:00 2001 From: Stanimira Rikova Date: Thu, 6 Feb 2025 01:07:14 +0200 Subject: [PATCH 33/39] Move outside the collections Include Children and fix return all --- .../Google/GSuiteAdmin/DeviceDescription.ts | 24 +++++++++------ .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 30 +++++++------------ .../Google/GSuiteAdmin/SearchFunctions.ts | 7 ++--- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index 9c678244615e6..adbaa90e829f5 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -140,6 +140,21 @@ export const deviceFields: INodeProperties[] = [ default: 'basic', description: 'What subset of fields to fetch for this device', }, + { + displayName: 'Include Children', + name: 'includeChildOrgunits', + type: 'boolean', + default: false, + // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether + description: + 'Include devices from organizational units below your specified organizational unit', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['device'], + }, + }, + }, { displayName: 'Options', name: 'options', @@ -164,15 +179,6 @@ export const deviceFields: INodeProperties[] = [ description: 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression.', }, - { - displayName: 'Include Children', - name: 'includeChildOrgunits', - type: 'boolean', - default: false, - // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether - description: - 'Include devices from organizational units below your specified organizational unit', - }, { displayName: 'Order By', name: 'orderBy', diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index a0a550ea96cb8..c6da6c5df7cee 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -806,36 +806,28 @@ export class GSuiteAdmin implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i); const output = this.getNodeParameter('projection', 1); - + const includeChildren = this.getNodeParameter('includeChildOrgunits', i); const options = this.getNodeParameter('options', 2); qs.projection = output; Object.assign(qs, options); + qs.includeChildOrgunits = includeChildren; if (qs.customer === undefined) { qs.customer = 'my_customer'; } - if (returnAll) { - responseData = await googleApiRequestAllItems.call( - this, - 'chromeosdevices', - 'GET', - `/directory/v1/customer/${qs.customer}/devices/chromeos/`, - {}, - qs, - ); - } else { + if (!returnAll) { qs.maxResults = this.getNodeParameter('limit', i); - - responseData = await googleApiRequest.call( - this, - 'GET', - `/directory/v1/customer/${qs.customer}/devices/chromeos/`, - {}, - qs, - ); } + + responseData = await googleApiRequest.call( + this, + 'GET', + `/directory/v1/customer/${qs.customer}/devices/chromeos/`, + {}, + qs, + ); if (!responseData || responseData.length === 0) { return [this.helpers.returnJsonArray({})]; } diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts index 02c23c84e6c10..5557ceb2638d3 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts @@ -5,7 +5,7 @@ import type { INodeListSearchItems, } from 'n8n-workflow'; -import { googleApiRequestAllItems } from './GenericFunctions'; +import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; export async function searchUsers(this: ILoadOptionsFunctions): Promise { const qs: IDataObject = { @@ -75,11 +75,10 @@ export async function searchDevices(this: ILoadOptionsFunctions): Promise Date: Thu, 6 Feb 2025 01:42:11 +0200 Subject: [PATCH 34/39] add collection sort and filter --- .../Google/GSuiteAdmin/DeviceDescription.ts | 134 ++++++++++-------- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 29 +++- 2 files changed, 101 insertions(+), 62 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index adbaa90e829f5..83244a2452c2b 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -87,7 +87,6 @@ export const deviceFields: INodeProperties[] = [ name: 'returnAll', type: 'boolean', default: false, - // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether description: 'Whether to return all results or only up to a given limit', displayOptions: { show: { @@ -145,9 +144,8 @@ export const deviceFields: INodeProperties[] = [ name: 'includeChildOrgunits', type: 'boolean', default: false, - // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether description: - 'Include devices from organizational units below your specified organizational unit', + 'Whether include devices from organizational units below your specified organizational unit', displayOptions: { show: { operation: ['getAll'], @@ -156,10 +154,10 @@ export const deviceFields: INodeProperties[] = [ }, }, { - displayName: 'Options', - name: 'options', + displayName: 'Filter', + name: 'filter', type: 'collection', - placeholder: 'Add Option', + placeholder: 'Add Filter', default: {}, displayOptions: { show: { @@ -177,71 +175,89 @@ export const deviceFields: INodeProperties[] = [ }, default: [], description: - 'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an expression. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression.', + 'Specify the organizational unit name or ID. Choose from the list or use an expression. Choose from the list, or specify an ID using an expression.', }, { - displayName: 'Order By', - name: 'orderBy', - type: 'options', - options: [ - { - name: 'Annotated Location', - value: 'annotatedLocation', - }, - { - name: 'Annotated User', - value: 'annotatedUser', - }, - { - name: 'Last Sync', - value: 'lastSync', - }, - { - name: 'Notes', - value: 'notes', - }, - { - name: 'Serial Number', - value: 'serialNumber', - }, - { - name: 'Status', - value: 'status', - }, - { - name: 'Support End Date', - value: 'supportEndDate', - }, - ], + displayName: 'Query', + name: 'query', + type: 'string', + placeholder: 'e.g. name:contact* email:contact*', default: '', - description: 'Property to use for sorting results', + description: "Use Google's querying syntax to filter results", }, + ], + }, + { + displayName: 'Sort', + name: 'sort', + type: 'fixedCollection', + placeholder: 'Add Sort Rule', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['device'], + }, + }, + options: [ { - displayName: 'Sort By', - name: 'sortBy', - type: 'options', - options: [ + name: 'sortRules', + displayName: 'Sort Rules', + values: [ { - name: 'Ascending', - value: 'ascending', + displayName: 'Order By', + name: 'orderBy', + type: 'options', + options: [ + { + name: 'Annotated Location', + value: 'annotatedLocation', + }, + { + name: 'Annotated User', + value: 'annotatedUser', + }, + { + name: 'Last Sync', + value: 'lastSync', + }, + { + name: 'Notes', + value: 'notes', + }, + { + name: 'Serial Number', + value: 'serialNumber', + }, + { + name: 'Status', + value: 'status', + }, + ], + default: '', + description: 'Field to sort the results by', }, { - name: 'Descending', - value: 'descending', + displayName: 'Sort Order', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ascending', + }, + { + name: 'Descending', + value: 'descending', + }, + ], + default: '', + description: 'Sort order direction', }, ], - default: '', - description: 'Property to use for sorting results. Must accompany Order By variable.', - }, - { - displayName: 'Query', - name: 'query', - type: 'string', - placeholder: 'e.g. name:contact* email:contact*', - default: '', - description: "Must use Google's querying syntax", }, ], + description: 'Define sorting rules for the results', }, /* -------------------------------------------------------------------------- */ /* device:update...... */ diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index c6da6c5df7cee..a311f1055a6cb 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -12,7 +12,7 @@ import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-wo import { deviceFields, deviceOperations } from './DeviceDescription'; import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; import { groupFields, groupOperations } from './GroupDescripion'; -import { searchGroups, searchUsers } from './SearchFunctions'; +import { searchDevices, searchGroups, searchUsers } from './SearchFunctions'; import { userFields, userOperations } from './UserDescription'; export class GSuiteAdmin implements INodeType { @@ -172,6 +172,7 @@ export class GSuiteAdmin implements INodeType { listSearch: { searchGroups, searchUsers, + searchDevices, }, }; @@ -807,16 +808,38 @@ export class GSuiteAdmin implements INodeType { const returnAll = this.getNodeParameter('returnAll', i); const output = this.getNodeParameter('projection', 1); const includeChildren = this.getNodeParameter('includeChildOrgunits', i); - const options = this.getNodeParameter('options', 2); + const filter = this.getNodeParameter('filter', i, {}) as IDataObject; + const sort = this.getNodeParameter('sort', i, {}) as IDataObject; qs.projection = output; - Object.assign(qs, options); qs.includeChildOrgunits = includeChildren; if (qs.customer === undefined) { qs.customer = 'my_customer'; } + if (filter.orgUnitPath) { + qs.orgUnitPath = filter.orgUnitPath as string; + } + if (filter.query && typeof filter.query === 'string') { + const query = filter.query.trim(); + if (query) { + qs.query = query; + } + } + if (sort.sortRules) { + const { orderBy, sortOrder } = sort.sortRules as { + orderBy?: string; + sortOrder?: string; + }; + if (orderBy) { + qs.orderBy = orderBy; + } + if (sortOrder) { + qs.sortOrder = sortOrder; + } + } + if (!returnAll) { qs.maxResults = this.getNodeParameter('limit', i); } From 84fd5e99daf2a63320fb5636bcbd6e9640e67f9a Mon Sep 17 00:00:00 2001 From: Stamsy Date: Tue, 11 Feb 2025 02:26:34 +0200 Subject: [PATCH 35/39] fix search device function --- .../nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts index 5557ceb2638d3..eac0623490446 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/SearchFunctions.ts @@ -53,7 +53,6 @@ export async function searchGroups(this: ILoadOptionsFunctions): Promise ({ name: device.serialNumber || 'Unknown Device', value: device.deviceId, From 2f88b87dcf570a51fc247df7f7e08bb5549f4517 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Tue, 11 Feb 2025 03:19:55 +0200 Subject: [PATCH 36/39] add unit test for searchDevice function --- .../GSuiteAdmin/test/SearchDevices.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchDevices.test.ts diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchDevices.test.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchDevices.test.ts new file mode 100644 index 0000000000000..672b90346e504 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/test/SearchDevices.test.ts @@ -0,0 +1,48 @@ +import type { ILoadOptionsFunctions } from 'n8n-workflow'; + +import { googleApiRequest } from '../GenericFunctions'; +import { searchDevices } from '../SearchFunctions'; + +jest.mock('../GenericFunctions'); + +describe('SearchFunctions - searchDevices', () => { + const mockContext = {} as unknown as ILoadOptionsFunctions; + + beforeEach(() => { + (googleApiRequest as jest.Mock).mockClear(); + }); + + it('should return a list of ChromeOS devices when googleApiRequest returns devices', async () => { + (googleApiRequest as jest.Mock).mockResolvedValueOnce({ + chromeosdevices: [ + { deviceId: 'device123', serialNumber: 'SN123' }, + { deviceId: 'device456', serialNumber: 'SN456' }, + ], + }); + + const result = await searchDevices.call(mockContext); + + expect(result).toEqual({ + results: [ + { name: 'SN123', value: 'device123' }, + { name: 'SN456', value: 'device456' }, + ], + }); + expect(googleApiRequest).toHaveBeenCalledTimes(1); + expect(googleApiRequest).toHaveBeenCalledWith( + 'GET', + '/directory/v1/customer/my_customer/devices/chromeos/', + {}, + { customerId: 'my_customer' }, + ); + }); + + it('should return an empty array when googleApiRequest response is undefined', async () => { + (googleApiRequest as jest.Mock).mockResolvedValueOnce(undefined); + + const result = await searchDevices.call(mockContext); + + expect(result).toEqual({ results: [] }); + expect(googleApiRequest).toHaveBeenCalledTimes(1); + }); +}); From a58e4354fa2a8031c54c4fe2c67be3b2f42e96b2 Mon Sep 17 00:00:00 2001 From: Stamsy Date: Tue, 11 Feb 2025 12:01:26 +0200 Subject: [PATCH 37/39] get deviceId --- .../nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index a311f1055a6cb..6d45a38e0137f 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -783,7 +783,8 @@ export class GSuiteAdmin implements INodeType { if (resource === 'device') { //https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/deviceId if (operation === 'get') { - const deviceId = this.getNodeParameter('deviceId', i) as string; + const deviceIdObject = this.getNodeParameter('deviceId', i) as IDataObject; + const deviceId = deviceIdObject.value as string; const output = this.getNodeParameter('projection', 1); // Validate deviceId @@ -859,7 +860,8 @@ export class GSuiteAdmin implements INodeType { } if (operation === 'update') { - const deviceId = this.getNodeParameter('deviceId', i) as string; + const deviceIdObject = this.getNodeParameter('deviceId', i) as IDataObject; + const deviceId = deviceIdObject.value as string; const updateOptions = this.getNodeParameter('updateOptions', 1); // Validate deviceId @@ -880,7 +882,8 @@ export class GSuiteAdmin implements INodeType { } if (operation === 'changeStatus') { - const deviceId = this.getNodeParameter('deviceId', i) as string; + const deviceIdObject = this.getNodeParameter('deviceId', i) as IDataObject; + const deviceId = deviceIdObject.value as string; const action = this.getNodeParameter('action', 1); qs.action = action; From bf645347d25875dc7b351dba0de00eb97891fbe8 Mon Sep 17 00:00:00 2001 From: Giulio Andreini Date: Thu, 13 Feb 2025 18:04:13 +0100 Subject: [PATCH 38/39] Copy tweak. --- .../nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index 6d45a38e0137f..b36cf52cc9bb8 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -408,11 +408,10 @@ export class GSuiteAdmin implements INodeType { const additionalFields = this.getNodeParameter('additionalFields', i); if (!username) { - throw new NodeOperationError( - this.getNode(), - 'The parameter ‘Username’ is empty. Please fill in the ‘Username’ parameter to create the user.', - { itemIndex: i }, - ); + throw new NodeOperationError(this.getNode(), 'The parameter ‘Username’ is empty', { + itemIndex: i, + description: 'Please fill in the ‘Username’ parameter to create the user', + }); } const body: IDataObject = { name: { From a068023d71fcf0b60b6a556d2d8c07d0664017dd Mon Sep 17 00:00:00 2001 From: Giulio Andreini Date: Fri, 14 Feb 2025 09:22:41 +0100 Subject: [PATCH 39/39] Renamed Device to ChromeOS Device. --- .../Google/GSuiteAdmin/DeviceDescription.ts | 16 ++++++++-------- .../nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts index 83244a2452c2b..5883f49963830 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/DeviceDescription.ts @@ -15,26 +15,26 @@ export const deviceOperations: INodeProperties[] = [ { name: 'Get', value: 'get', - description: 'Get a device', - action: 'Get a device', + description: 'Get a ChromeOS device', + action: 'Get ChromeOS device', }, { name: 'Get Many', value: 'getAll', - description: 'Get many devices', - action: 'Get many devices', + description: 'Get many ChromeOS devices', + action: 'Get many ChromeOS devices', }, { name: 'Update', value: 'update', - description: 'Update a device', - action: 'Update a device', + description: 'Update a ChromeOS device', + action: 'Update ChromeOS device', }, { name: 'Change Status', value: 'changeStatus', - description: 'Change the Status of a Chromebook', - action: 'Set the status of a device', + description: 'Change the status of a ChromeOS device', + action: 'Change status of ChromeOS device', }, ], default: 'get', diff --git a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts index b36cf52cc9bb8..a6a65e8ca081c 100644 --- a/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts +++ b/packages/nodes-base/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.ts @@ -52,7 +52,7 @@ export class GSuiteAdmin implements INodeType { value: 'user', }, { - name: 'Device', + name: 'ChromeOS Device', value: 'device', }, ],