feat(Google Workspace Admin Node): Google Admin Node Overhaul implementation (#12271)

Co-authored-by: knowa <github@libertyunion.org>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
Stanimira Rikova
2025-05-02 19:27:08 +03:00
committed by GitHub
parent bd258be052
commit 8a30c35c33
44 changed files with 5452 additions and 454 deletions

View File

@@ -5,6 +5,8 @@ const scopes = [
'https://www.googleapis.com/auth/admin.directory.user', 'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.domain.readonly', '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.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 { export class GSuiteAdminOAuth2Api implements ICredentialType {

View File

@@ -0,0 +1,361 @@
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 ChromeOS device',
action: 'Get ChromeOS device',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many ChromeOS devices',
action: 'Get many ChromeOS devices',
},
{
name: 'Update',
value: 'update',
description: 'Update a ChromeOS device',
action: 'Update ChromeOS device',
},
{
name: 'Change Status',
value: 'changeStatus',
description: 'Change the status of a ChromeOS device',
action: 'Change status of ChromeOS device',
},
],
default: 'get',
},
];
export const deviceFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* device */
/* -------------------------------------------------------------------------- */
{
displayName: 'Device',
name: 'deviceId',
type: 'resourceLocator',
required: true,
displayOptions: {
show: {
operation: ['get', 'update', 'changeStatus'],
resource: ['device'],
},
},
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:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* device:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
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: 'Output',
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'],
resource: ['device'],
},
},
default: 'basic',
description: 'What subset of fields to fetch for this device',
},
{
displayName: 'Include Children',
name: 'includeChildOrgunits',
type: 'boolean',
default: false,
description:
'Whether to include devices from organizational units below your specified organizational unit',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['device'],
},
},
},
{
displayName: 'Filter',
name: 'filter',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
resource: ['device'],
},
},
options: [
{
displayName: 'Organizational Unit Name or ID',
name: 'orgUnitPath',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrgUnits',
},
default: [],
description:
'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 <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
placeholder: 'e.g. name:contact* email:contact*',
default: '',
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: [
{
name: 'sortRules',
displayName: 'Sort Rules',
values: [
{
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',
},
{
displayName: 'Sort Order',
name: 'sortBy',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ascending',
},
{
name: 'Descending',
value: 'descending',
},
],
default: '',
description: 'Sort order direction',
},
],
},
],
description: 'Define sorting rules for the results',
},
/* -------------------------------------------------------------------------- */
/* device:update */
/* -------------------------------------------------------------------------- */
{
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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Annotated User',
name: 'annotatedUser',
type: 'string',
default: '',
description: 'The annotated user of the device',
placeholder: 'e.g. help desk',
},
{
displayName: 'Annotated Location',
name: 'annotatedLocation',
type: 'string',
default: '',
description: 'The annotated location of the device',
placeholder: 'e.g. Mountain View help desk Chromebook',
},
{
displayName: 'Annotated Asset ID',
name: 'annotatedAssetId',
type: 'string',
default: '',
description: 'The annotated asset ID of a device',
placeholder: 'e.g. 1234567890',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
description: 'Add notes to a device',
placeholder: 'e.g. Loaned from support',
},
],
},
/* -------------------------------------------------------------------------- */
/* device:changeStatus */
/* -------------------------------------------------------------------------- */
{
displayName: 'Status',
name: 'action',
type: 'options',
required: true,
options: [
{
name: 'Enabled',
value: 'reenable',
description: 'Re-enable a disabled chromebook',
action: 'Enable a device',
},
{
name: 'Disabled',
value: 'disable',
description: 'Disable a chromebook',
action: 'Disable a device',
},
],
displayOptions: {
show: {
operation: ['changeStatus'],
resource: ['device'],
},
},
default: 'reenable',
description: 'Set the status of a device',
},
];

View File

@@ -9,16 +9,17 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow'; import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { deviceFields, deviceOperations } from './DeviceDescription';
import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions';
import { groupFields, groupOperations } from './GroupDescripion'; import { groupFields, groupOperations } from './GroupDescripion';
import { searchDevices, searchGroups, searchUsers } from './SearchFunctions';
import { userFields, userOperations } from './UserDescription'; import { userFields, userOperations } from './UserDescription';
export class GSuiteAdmin implements INodeType { export class GSuiteAdmin implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Google Workspace Admin', displayName: 'Google Workspace Admin',
name: 'gSuiteAdmin', name: 'gSuiteAdmin',
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg icon: 'file:gSuiteAdmin.svg',
icon: 'file:google-workspace-admin.png',
group: ['input'], group: ['input'],
version: 1, version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
@@ -42,6 +43,10 @@ export class GSuiteAdmin implements INodeType {
type: 'options', type: 'options',
noDataExpression: true, noDataExpression: true,
options: [ options: [
{
name: 'ChromeOS Device',
value: 'device',
},
{ {
name: 'Group', name: 'Group',
value: 'group', value: 'group',
@@ -53,6 +58,8 @@ export class GSuiteAdmin implements INodeType {
], ],
default: 'user', default: 'user',
}, },
...deviceOperations,
...deviceFields,
...groupOperations, ...groupOperations,
...groupFields, ...groupFields,
...userOperations, ...userOperations,
@@ -62,16 +69,16 @@ export class GSuiteAdmin implements INodeType {
methods = { methods = {
loadOptions: { loadOptions: {
// Get all the domains to display them to user so that they can
// select them easily
async getDomains(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getDomains(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const returnData: INodePropertyOptions[] = [];
const domains = await googleApiRequestAllItems.call( const domains = (await googleApiRequestAllItems.call(
this, this,
'domains', 'domains',
'GET', 'GET',
'/directory/v1/customer/my_customer/domains', '/directory/v1/customer/my_customer/domains',
); )) as Array<{
domainName: string;
}>;
for (const domain of domains) { for (const domain of domains) {
const domainName = domain.domainName; const domainName = domain.domainName;
const domainId = domain.domainName; const domainId = domain.domainName;
@@ -82,47 +89,180 @@ export class GSuiteAdmin implements INodeType {
} }
return returnData; return returnData;
}, },
// Get all the schemas to display them to user so that they can
// select them easily
async getSchemas(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { async getSchemas(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = []; const schemas = (await googleApiRequestAllItems.call(
const schemas = await googleApiRequestAllItems.call(
this, this,
'schemas', 'schemas',
'GET', 'GET',
'/directory/v1/customer/my_customer/schemas', '/directory/v1/customer/my_customer/schemas',
); )) as Array<{
for (const schema of schemas) { displayName: string;
const schemaName = schema.displayName; schemaName: string;
const schemaId = schema.schemaName; }>;
return schemas.map((schema: { schemaName: string; displayName: string }) => ({
name: schema.displayName || schema.schemaName,
value: schema.schemaName,
}));
},
async getOrgUnits(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const orgUnits = (await googleApiRequest.call(
this,
'GET',
'/directory/v1/customer/my_customer/orgunits',
{},
{ orgUnitPath: '/', type: 'all' },
)) as {
organizationUnits: Array<{
name: string;
orgUnitPath: string;
}>;
};
for (const unit of orgUnits.organizationUnits) {
returnData.push({ returnData.push({
name: schemaName, name: unit.name,
value: schemaId, value: unit.orgUnitPath,
}); });
} }
return returnData; return returnData;
}, },
}, },
listSearch: {
searchDevices,
searchGroups,
searchUsers,
},
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
const length = items.length; const length = items.length;
const qs: IDataObject = {};
let responseData; let responseData;
const resource = this.getNodeParameter('resource', 0); const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0); const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const qs: IDataObject = {};
try { try {
if (resource === 'group') { 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, undefined, {
extractValue: true,
}) as string;
const output = this.getNodeParameter('projection', 1);
responseData = await googleApiRequest.call(
this,
'GET',
`/directory/v1/customer/my_customer/devices/chromeos/${deviceId}?projection=${output}`,
{},
);
}
//https://developers.google.com/admin-sdk/directory/reference/rest/v1/chromeosdevices/list
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const output = this.getNodeParameter('projection', 1);
const includeChildren = this.getNodeParameter('includeChildOrgunits', i);
const filter = this.getNodeParameter('filter', i, {}) as {
query?: string;
orgUnitPath?: string;
};
const sort = this.getNodeParameter('sort', i, {}) as {
sortRules?: {
orderBy?: string;
sortOrder?: string;
};
};
qs.projection = output;
qs.includeChildOrgunits = includeChildren;
if (qs.customer === undefined) {
qs.customer = 'my_customer';
}
if (filter.orgUnitPath) {
qs.orgUnitPath = filter.orgUnitPath;
}
if (filter.query) {
qs.query = filter.query.trim();
}
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) {
qs.orderBy = orderBy;
}
if (sortOrder) {
qs.sortOrder = sortOrder;
}
}
if (!returnAll) {
qs.maxResults = this.getNodeParameter('limit', i);
}
responseData = await googleApiRequest.call(
this,
'GET',
`/directory/v1/customer/${qs.customer}/devices/chromeos/`,
{},
qs,
);
if (!Array.isArray(responseData) || responseData.length === 0) {
return [this.helpers.returnJsonArray({})];
}
return [this.helpers.returnJsonArray(responseData)];
}
if (operation === 'update') {
const deviceId = this.getNodeParameter('deviceId', i, undefined, {
extractValue: true,
}) as string;
const updateOptions = this.getNodeParameter('updateOptions', 1);
Object.assign(qs, updateOptions);
responseData = await googleApiRequest.call(
this,
'PUT',
`/directory/v1/customer/my_customer/devices/chromeos/${deviceId}`,
qs,
);
}
if (operation === 'changeStatus') {
const deviceId = this.getNodeParameter('deviceId', i, undefined, {
extractValue: true,
}) as string;
const action = this.getNodeParameter('action', 1);
qs.action = action;
responseData = await googleApiRequest.call(
this,
'POST',
`/directory/v1/customer/my_customer/devices/chromeos/${deviceId}/action`,
qs,
);
}
} else if (resource === 'group') {
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/insert //https://developers.google.com/admin-sdk/directory/v1/reference/groups/insert
if (operation === 'create') { if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const email = this.getNodeParameter('email', i) as string; const email = this.getNodeParameter('email', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i); const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = { const body: IDataObject = {
name,
email, email,
}; };
@@ -133,21 +273,20 @@ export class GSuiteAdmin implements INodeType {
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/delete //https://developers.google.com/admin-sdk/directory/v1/reference/groups/delete
if (operation === 'delete') { if (operation === 'delete') {
const groupId = this.getNodeParameter('groupId', i) as string; const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
responseData = await googleApiRequest.call( await googleApiRequest.call(this, 'DELETE', `/directory/v1/groups/${groupId}`, {});
this,
'DELETE',
`/directory/v1/groups/${groupId}`,
{},
);
responseData = { success: true }; responseData = { success: true };
} }
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/get //https://developers.google.com/admin-sdk/directory/v1/reference/groups/get
if (operation === 'get') { if (operation === 'get') {
const groupId = this.getNodeParameter('groupId', i) as string; const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
this, this,
@@ -156,19 +295,66 @@ export class GSuiteAdmin implements INodeType {
{}, {},
); );
} }
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/list //https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
if (operation === 'getAll') { if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i); const returnAll = this.getNodeParameter('returnAll', i);
const filter = this.getNodeParameter('filter', i, {}) as {
customer?: string;
domain?: string;
query?: string;
userId?: string;
};
const sort = this.getNodeParameter('sort', i, {}) as {
sortRules?: {
orderBy?: string;
sortOrder?: string;
};
};
const options = this.getNodeParameter('options', i); if (filter.customer) {
qs.customer = filter.customer;
}
Object.assign(qs, options); if (filter.domain) {
qs.domain = filter.domain;
}
if (qs.customer === undefined) { if (filter.query) {
const query = filter.query.trim();
const regex = /^(name|email):\S+$/;
if (!regex.test(query)) {
throw new NodeOperationError(
this.getNode(),
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
}
qs.query = query;
}
if (filter.userId) {
qs.userId = filter.userId;
}
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) {
qs.orderBy = orderBy;
}
if (sortOrder) {
qs.sortOrder = sortOrder;
}
}
if (!qs.customer) {
qs.customer = 'my_customer'; qs.customer = 'my_customer';
} }
if (!returnAll) {
qs.maxResults = this.getNodeParameter('limit', i);
}
if (returnAll) { if (returnAll) {
responseData = await googleApiRequestAllItems.call( responseData = await googleApiRequestAllItems.call(
this, this,
@@ -180,7 +366,6 @@ export class GSuiteAdmin implements INodeType {
); );
} else { } else {
qs.maxResults = this.getNodeParameter('limit', i); qs.maxResults = this.getNodeParameter('limit', i);
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
this, this,
'GET', 'GET',
@@ -188,14 +373,15 @@ export class GSuiteAdmin implements INodeType {
{}, {},
qs, qs,
); );
responseData = responseData.groups || [];
responseData = responseData.groups;
} }
} }
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/update //https://developers.google.com/admin-sdk/directory/v1/reference/groups/update
if (operation === 'update') { if (operation === 'update') {
const groupId = this.getNodeParameter('groupId', i) as string; const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
const updateFields = this.getNodeParameter('updateFields', i); const updateFields = this.getNodeParameter('updateFields', i);
@@ -210,23 +396,62 @@ export class GSuiteAdmin implements INodeType {
body, body,
); );
} }
} else if (resource === 'user') {
//https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/insert
if (operation === 'addToGroup') {
const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
let userEmail: string | undefined;
// If the user ID is not already an email, fetch the user details
if (!userId.includes('@')) {
const userDetails = (await googleApiRequest.call(
this,
'GET',
`/directory/v1/users/${userId}`,
)) as {
primaryEmail: string;
};
userEmail = userDetails.primaryEmail;
} else {
userEmail = userId;
}
if (!userEmail) {
throw new NodeOperationError(
this.getNode(),
'Unable to determine the user email for adding to the group',
{ itemIndex: i },
);
}
const body: IDataObject = {
email: userEmail,
role: 'MEMBER',
};
await googleApiRequest.call(
this,
'POST',
`/directory/v1/groups/${groupId}/members`,
body,
);
responseData = { added: true };
} }
if (resource === 'user') {
//https://developers.google.com/admin-sdk/directory/v1/reference/users/insert //https://developers.google.com/admin-sdk/directory/v1/reference/users/insert
if (operation === 'create') { if (operation === 'create') {
const domain = this.getNodeParameter('domain', i) as string; const domain = this.getNodeParameter('domain', i) as string;
const firstName = this.getNodeParameter('firstName', i) as string; const firstName = this.getNodeParameter('firstName', i) as string;
const lastName = this.getNodeParameter('lastName', i) as string; const lastName = this.getNodeParameter('lastName', i) as string;
const password = this.getNodeParameter('password', i) as string; const password = this.getNodeParameter('password', i) as string;
const username = this.getNodeParameter('username', 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 additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = { const body: IDataObject = {
@@ -238,22 +463,63 @@ export class GSuiteAdmin implements INodeType {
primaryEmail: `${username}@${domain}`, primaryEmail: `${username}@${domain}`,
}; };
Object.assign(body, additionalFields); if (!username) {
throw new NodeOperationError(this.getNode(), "The parameter 'Username' is empty", {
itemIndex: i,
description: "Please fill in the 'Username' parameter to create the user",
});
}
if (additionalFields.phoneUi) { if (additionalFields.phoneUi) {
const phones = (additionalFields.phoneUi as IDataObject).phoneValues as IDataObject[]; body.phones = (additionalFields.phoneUi as IDataObject).phoneValues as IDataObject[];
body.phones = phones;
delete body.phoneUi;
} }
if (additionalFields.emailUi) { if (additionalFields.emailUi) {
const emails = (additionalFields.emailUi as IDataObject).emailValues as IDataObject[]; body.emails = (additionalFields.emailUi as IDataObject).emailValues as IDataObject[];
}
body.emails = emails; if (additionalFields.roles) {
const roles = additionalFields.roles as string[];
body.roles = {
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'),
};
}
delete body.emailUi; if (additionalFields.customFields) {
const customFields = (additionalFields.customFields as IDataObject)
.fieldValues as IDataObject[];
const customSchemas: IDataObject = {};
customFields.forEach((field) => {
const { schemaName, fieldName, value } = field as {
schemaName: string;
fieldName: string;
value: string;
};
if (!schemaName || !fieldName || !value) {
throw new NodeOperationError(this.getNode(), 'Invalid custom field data', {
itemIndex: i,
description: 'Schema name, field name, and value are required.',
});
}
customSchemas[schemaName] ??= {};
(customSchemas[schemaName] as IDataObject)[fieldName] = value;
});
if (Object.keys(customSchemas).length > 0) {
body.customSchemas = customSchemas;
}
} }
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
@@ -263,22 +529,13 @@ export class GSuiteAdmin implements INodeType {
body, body,
qs, 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 //https://developers.google.com/admin-sdk/directory/v1/reference/users/delete
if (operation === 'delete') { if (operation === 'delete') {
const userId = this.getNodeParameter('userId', i) as string; const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
this, this,
@@ -287,31 +544,30 @@ export class GSuiteAdmin implements INodeType {
{}, {},
); );
responseData = { success: true }; responseData = { deleted: true };
} }
//https://developers.google.com/admin-sdk/directory/v1/reference/users/get //https://developers.google.com/admin-sdk/directory/v1/reference/users/get
if (operation === 'get') { if (operation === 'get') {
const userId = this.getNodeParameter('userId', i) as string; const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
const projection = this.getNodeParameter('projection', i) as string; const output = this.getNodeParameter('output', i);
const projection = this.getNodeParameter('projection', i);
const options = this.getNodeParameter('options', i); const fields = this.getNodeParameter('fields', i, []) as string[];
if (projection) {
qs.projection = projection; qs.projection = projection;
Object.assign(qs, options);
if (qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(' ');
} }
if (projection === 'custom' && qs.customFieldMask) {
if (qs.projection === 'custom' && qs.customFieldMask === undefined) { qs.customFieldMask = (qs.customFieldMask as string[]).join(',');
throw new NodeOperationError( }
this.getNode(), if (output === 'select') {
'When projection is set to custom, the custom schemas field must be defined', if (!fields.includes('id')) {
{ itemIndex: i }, fields.push('id');
); }
qs.fields = fields.join(',');
} }
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
@@ -321,36 +577,82 @@ export class GSuiteAdmin implements INodeType {
{}, {},
qs, 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 //https://developers.google.com/admin-sdk/directory/v1/reference/users/list
if (operation === 'getAll') { if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i); 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 projection = this.getNodeParameter('projection', i) as string;
const filter = this.getNodeParameter('filter', i, {}) as {
customer?: string;
domain?: string;
query?: string;
showDeleted?: boolean;
};
const sort = this.getNodeParameter('sort', i, {}) as {
sortRules?: {
orderBy?: string;
sortOrder?: string;
};
};
const options = this.getNodeParameter('options', i); if (filter.customer) {
qs.customer = filter.customer;
}
if (filter.domain) {
qs.domain = filter.domain;
}
if (filter.query) {
qs.query = filter.query.trim();
}
if (filter.showDeleted) {
qs.showDeleted = filter.showDeleted ? 'true' : 'false';
}
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) {
qs.orderBy = orderBy;
}
if (sortOrder) {
qs.sortOrder = sortOrder;
}
}
qs.projection = projection; qs.projection = projection;
if (projection === 'custom' && qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(',');
}
Object.assign(qs, options); if (output === 'select') {
if (!fields.includes('id')) {
fields.push('id');
}
qs.fields = `users(${fields.join(',')})`;
}
if (qs.customer === undefined) { if (!qs.customer) {
qs.customer = 'my_customer'; qs.customer = 'my_customer';
} }
if (qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(' ');
}
if (qs.projection === 'custom' && qs.customFieldMask === undefined) {
throw new NodeOperationError(
this.getNode(),
'When projection is set to custom, the custom schemas field must be defined',
{ itemIndex: i },
);
}
if (returnAll) { if (returnAll) {
responseData = await googleApiRequestAllItems.call( responseData = await googleApiRequestAllItems.call(
this, this,
@@ -362,7 +664,6 @@ export class GSuiteAdmin implements INodeType {
); );
} else { } else {
qs.maxResults = this.getNodeParameter('limit', i); qs.maxResults = this.getNodeParameter('limit', i);
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
this, this,
'GET', 'GET',
@@ -373,55 +674,124 @@ export class GSuiteAdmin implements INodeType {
responseData = responseData.users; 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 groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
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 //https://developers.google.com/admin-sdk/directory/v1/reference/users/update
if (operation === 'update') { if (operation === 'update') {
const userId = this.getNodeParameter('userId', i) as string; const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
const updateFields = this.getNodeParameter('updateFields', i); const updateFields = this.getNodeParameter('updateFields', i);
const body: { const body: {
name: { givenName?: string; familyName?: string }; name?: { givenName?: string; familyName?: string };
emails?: IDataObject[]; emails?: IDataObject[];
primaryEmail?: string;
phones?: IDataObject[]; phones?: IDataObject[];
} = { name: {} }; suspended?: boolean;
roles?: { [key: string]: boolean };
Object.assign(body, updateFields); customSchemas?: IDataObject;
} = {};
if (updateFields.firstName) { if (updateFields.firstName) {
body.name ??= {};
body.name.givenName = updateFields.firstName as string; body.name.givenName = updateFields.firstName as string;
//@ts-ignore
delete body.firstName;
} }
if (updateFields.lastName) { if (updateFields.lastName) {
body.name ??= {};
body.name.familyName = updateFields.lastName as string; 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) { if (updateFields.phoneUi) {
const phones = (updateFields.phoneUi as IDataObject).phoneValues as IDataObject[]; body.phones = (updateFields.phoneUi as IDataObject).phoneValues as IDataObject[];
body.phones = phones;
//@ts-ignore
delete body.phoneUi;
} }
if (updateFields.emailUi) { if (updateFields.emailUi) {
const emails = (updateFields.emailUi as IDataObject).emailValues as IDataObject[]; body.emails = (updateFields.emailUi as IDataObject).emailValues as IDataObject[];
}
body.emails = emails; if (updateFields.primaryEmail) {
body.primaryEmail = updateFields.primaryEmail as string;
}
//@ts-ignore if (updateFields.suspendUi) {
delete body.emailUi; body.suspended = updateFields.suspendUi as boolean;
}
if (updateFields.roles) {
const roles = updateFields.roles as string[];
body.roles = {
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'),
};
}
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: string;
};
if (!schemaName || !fieldName || !value) {
throw new NodeOperationError(this.getNode(), 'Invalid custom field data', {
itemIndex: i,
description: 'Schema name, field name, and value are required.',
});
}
customSchemas[schemaName] ??= {};
(customSchemas[schemaName] as IDataObject)[fieldName] = value;
});
if (Object.keys(customSchemas).length > 0) {
body.customSchemas = customSchemas;
}
} }
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
@@ -441,17 +811,32 @@ export class GSuiteAdmin implements INodeType {
returnData.push(...executionData); returnData.push(...executionData);
} catch (error) { } catch (error) {
if (error instanceof NodeOperationError) {
throw error;
}
if (this.continueOnFail()) { if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData( 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 } }, { itemData: { item: i } },
); );
returnData.push(...executionErrorData); returnData.push(...executionErrorData);
continue; 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.description}`,
itemIndex: i,
},
);
} }
} }
return [returnData]; return [returnData];
} }
} }

View File

@@ -4,7 +4,7 @@ import type {
IDataObject, IDataObject,
JsonObject, JsonObject,
IHttpRequestMethods, IHttpRequestMethods,
IRequestOptions, IHttpRequestOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow';
@@ -12,20 +12,19 @@ export async function googleApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions, this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods, method: IHttpRequestMethods,
resource: string, resource: string,
body: any = {}, body: any = {},
qs: IDataObject = {}, qs: IDataObject = {},
uri?: string, uri?: string,
headers: IDataObject = {}, headers: IDataObject = {},
): Promise<any> { ): Promise<any> {
const options: IRequestOptions = { const options: IHttpRequestOptions = {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
method, method,
body, body,
qs, qs,
uri: uri || `https://www.googleapis.com/admin${resource}`, url: uri || `https://www.googleapis.com/admin${resource}`,
json: true, json: true,
}; };
try { try {
@@ -35,7 +34,11 @@ export async function googleApiRequest(
if (Object.keys(body as IDataObject).length === 0) { if (Object.keys(body as IDataObject).length === 0) {
delete options.body; delete options.body;
} }
return await this.helpers.requestOAuth2.call(this, 'gSuiteAdminOAuth2Api', options); return await this.helpers.httpRequestWithAuthentication.call(
this,
'gSuiteAdminOAuth2Api',
options,
);
} catch (error) { } catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject); throw new NodeApiError(this.getNode(), error as JsonObject);
} }
@@ -46,7 +49,6 @@ export async function googleApiRequestAllItems(
propertyName: string, propertyName: string,
method: IHttpRequestMethods, method: IHttpRequestMethods,
endpoint: string, endpoint: string,
body: any = {}, body: any = {},
query: IDataObject = {}, query: IDataObject = {},
): Promise<any> { ): Promise<any> {

View File

@@ -48,14 +48,65 @@ export const groupOperations: INodeProperties[] = [
]; ];
export const groupFields: INodeProperties[] = [ export const groupFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* group */
/* -------------------------------------------------------------------------- */
{
displayName: 'Group',
name: 'groupId',
default: {
mode: 'list',
value: '',
},
description: 'Select the group to perform the operation on',
displayOptions: {
show: {
operation: ['delete', 'get', 'update'],
resource: ['group'],
},
},
modes: [
{
displayName: 'From list',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchGroups',
},
},
{
displayName: 'By ID',
name: 'GroupId',
type: 'string',
placeholder: 'e.g. 0123kx3o1habcdf',
},
],
required: true,
type: 'resourceLocator',
},
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* group:create */ /* group:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Email', 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: 'Group Email',
name: 'email', name: 'email',
type: 'string', type: 'string',
placeholder: 'name@email.com', placeholder: 'e.g. sales@example.com',
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
@@ -88,51 +139,13 @@ export const groupFields: INodeProperties[] = [
description: 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.', '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 */ /* group:delete */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{
displayName: 'Group ID',
name: 'groupId',
type: 'string',
required: true,
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.",
},
/* -------------------------------------------------------------------------- */
/* group:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Group ID',
name: 'groupId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['get'],
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.",
},
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* group:getAll */ /* group:getAll */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@@ -168,10 +181,10 @@ export const groupFields: INodeProperties[] = [
description: 'Max number of results to return', description: 'Max number of results to return',
}, },
{ {
displayName: 'Options', displayName: 'Filter',
name: 'options', name: 'filter',
type: 'collection', type: 'collection',
placeholder: 'Add option', placeholder: 'Add Filter',
default: {}, default: {},
displayOptions: { displayOptions: {
show: { show: {
@@ -185,16 +198,50 @@ export const groupFields: INodeProperties[] = [
name: 'customer', name: 'customer',
type: 'string', type: 'string',
default: '', default: '',
description: description: "The unique ID for the customer's Google Workspace account",
"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.",
}, },
{ {
displayName: 'Domain', displayName: 'Domain',
name: 'domain', name: 'domain',
type: 'string', type: 'string',
default: '', 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: 'Query',
name: 'query',
type: 'string',
placeholder: 'e.g. name:contact* email:contact*',
default: '',
description:
'Query string to filter the results. Follow Google Admin SDK documentation. <a href="https://developers.google.com/admin-sdk/directory/v1/guides/search-groups#examples" target="_blank">More info</a>.',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
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: [
{
name: 'sortRules',
displayName: 'Sort Rules',
values: [
{ {
displayName: 'Order By', displayName: 'Order By',
name: 'orderBy', name: 'orderBy',
@@ -205,16 +252,8 @@ export const groupFields: INodeProperties[] = [
value: 'email', value: 'email',
}, },
], ],
default: '', default: 'email',
description: 'Property to use for sorting results', description: 'Field to sort the results by',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description:
'Query string search. Complete documentation is <a href="https://developers.google.com/admin-sdk/directory/v1/guides/search-groups">at</a>.',
}, },
{ {
displayName: 'Sort Order', displayName: 'Sort Order',
@@ -230,37 +269,17 @@ export const groupFields: INodeProperties[] = [
value: 'DESCENDING', value: 'DESCENDING',
}, },
], ],
default: '', default: 'ASCENDING',
description: 'Whether to return results in ascending or descending order', description: 'Sort order direction',
},
{
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.",
}, },
], ],
}, },
],
},
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* group:update */ /* group:update */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{
displayName: 'Group ID',
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.",
},
{ {
displayName: 'Update Fields', displayName: 'Update Fields',
name: 'updateFields', name: 'updateFields',
@@ -279,6 +298,9 @@ export const groupFields: INodeProperties[] = [
name: 'description', name: 'description',
type: 'string', type: 'string',
default: '', default: '',
typeOptions: {
rows: 2,
},
description: 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.', '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.',
}, },
@@ -286,7 +308,7 @@ export const groupFields: INodeProperties[] = [
displayName: 'Email', displayName: 'Email',
name: 'email', name: 'email',
type: 'string', type: 'string',
placeholder: 'name@email.com', placeholder: 'e.g. sales@example.com',
default: '', default: '',
description: 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.", "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 +317,7 @@ export const groupFields: INodeProperties[] = [
displayName: 'Name', displayName: 'Name',
name: 'name', name: 'name',
type: 'string', type: 'string',
placeholder: 'e.g. Sales',
default: '', default: '',
description: "The group's display name", description: "The group's display name",
}, },

View File

@@ -0,0 +1,91 @@
import type {
ILoadOptionsFunctions,
IDataObject,
INodeListSearchResult,
INodeListSearchItems,
} from 'n8n-workflow';
import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions';
export async function searchUsers(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const qs: IDataObject = {
customer: 'my_customer',
};
const responseData = await googleApiRequestAllItems.call(
this,
'users',
'GET',
'/directory/v1/users',
{},
qs,
);
if (!Array.isArray(responseData)) {
return { results: [] };
}
const results: INodeListSearchItems[] = responseData.map(
(user: { name?: { fullName?: string }; id: string }) => ({
name: user.name?.fullName ?? user.id,
value: user.id,
}),
);
return { results };
}
export async function searchGroups(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const qs: IDataObject = {
customer: 'my_customer',
};
const responseData = await googleApiRequestAllItems.call(
this,
'groups',
'GET',
'/directory/v1/groups',
{},
qs,
);
if (!Array.isArray(responseData)) {
return { results: [] };
}
const results: INodeListSearchItems[] = responseData.map(
(group: { name?: string; email?: string; id: string }) => ({
name: group.name || group.email || 'Unnamed Group',
value: group.id,
}),
);
return { results };
}
export async function searchDevices(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const qs: IDataObject = {
customerId: 'my_customer',
};
const responseData = await googleApiRequest.call(
this,
'GET',
'/directory/v1/customer/my_customer/devices/chromeos/',
{},
qs,
);
if (!Array.isArray(responseData?.chromeosdevices)) {
return { results: [] };
}
const results: INodeListSearchItems[] = responseData.chromeosdevices.map(
(device: { deviceId: string; serialNumber?: string }) => ({
name: device.serialNumber || device.deviceId || 'Unknown Device',
value: device.deviceId,
}),
);
return { results };
}

View File

@@ -1,5 +1,63 @@
import type { INodeProperties } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow';
const rolesOptions = [
{
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',
},
];
export const userOperations: INodeProperties[] = [ export const userOperations: INodeProperties[] = [
{ {
displayName: 'Operation', displayName: 'Operation',
@@ -12,6 +70,12 @@ export const userOperations: INodeProperties[] = [
}, },
}, },
options: [ options: [
{
name: 'Add to Group',
value: 'addToGroup',
description: 'Add an existing user to a group',
action: 'Add user to group',
},
{ {
name: 'Create', name: 'Create',
value: 'create', value: 'create',
@@ -36,6 +100,12 @@ export const userOperations: INodeProperties[] = [
description: 'Get many users', description: 'Get many users',
action: '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', name: 'Update',
value: 'update', value: 'update',
@@ -48,12 +118,104 @@ export const userOperations: INodeProperties[] = [
]; ];
export const userFields: INodeProperties[] = [ export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user */
/* -------------------------------------------------------------------------- */
{
displayName: 'User',
name: 'userId',
required: true,
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
description: 'Select the user to perform the operation on',
displayOptions: {
show: {
resource: ['user'],
operation: ['addToGroup', 'delete', 'get', 'removeFromGroup', 'update'],
},
},
modes: [
{
displayName: 'From list',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchUsers',
},
},
{
displayName: 'By Email',
name: 'userEmail',
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',
name: 'userId',
type: 'string',
hint: 'Enter the user id',
placeholder: 'e.g. 123456789879230471055',
},
],
},
/* -------------------------------------------------------------------------- */
/* user:addToGroup */
/* -------------------------------------------------------------------------- */
{
displayName: 'Group',
name: 'groupId',
required: true,
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
description: 'Select the group to perform the operation on',
displayOptions: {
show: {
resource: ['user'],
operation: ['addToGroup', 'removeFromGroup'],
},
},
modes: [
{
displayName: 'From list',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchGroups',
},
},
{
displayName: 'By ID',
name: 'groupId',
type: 'string',
placeholder: 'e.g. 0123kx3o1habcdf',
},
],
},
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* user:create */ /* user:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'First Name', displayName: 'First Name',
name: 'firstName', name: 'firstName',
placeholder: 'e.g. Nathan',
type: 'string', type: 'string',
required: true, required: true,
displayOptions: { displayOptions: {
@@ -69,6 +231,7 @@ export const userFields: INodeProperties[] = [
name: 'lastName', name: 'lastName',
type: 'string', type: 'string',
required: true, required: true,
placeholder: 'e.g. Smith',
displayOptions: { displayOptions: {
show: { show: {
operation: ['create'], operation: ['create'],
@@ -95,6 +258,21 @@ export const userFields: INodeProperties[] = [
description: description:
'Stores the password for the user account. A minimum of 8 characters is required. The maximum length is 100 characters.', 'Stores the password for the user account. A minimum of 8 characters is required. The maximum length is 100 characters.',
}, },
{
displayName: 'Username',
name: 'username',
type: 'string',
placeholder: 'e.g. n.smith',
displayOptions: {
show: {
operation: ['create'],
resource: ['user'],
},
},
default: '',
description:
"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: 'Domain Name or ID', displayName: 'Domain Name or ID',
name: 'domain', name: 'domain',
@@ -113,34 +291,6 @@ export const userFields: INodeProperties[] = [
}, },
default: '', default: '',
}, },
{
displayName: 'Username',
name: 'username',
type: 'string',
displayOptions: {
show: {
operation: ['create'],
resource: ['user'],
},
},
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.",
},
{
displayName: 'Make Admin',
name: 'makeAdmin',
type: 'boolean',
required: true,
displayOptions: {
show: {
operation: ['create'],
resource: ['user'],
},
},
default: false,
description: 'Whether to make a user a super administrator',
},
{ {
displayName: 'Additional Fields', displayName: 'Additional Fields',
name: 'additionalFields', name: 'additionalFields',
@@ -155,9 +305,8 @@ export const userFields: INodeProperties[] = [
}, },
options: [ options: [
{ {
displayName: 'Change Password At Next Login', displayName: 'Change Password at Next Login',
name: 'changePasswordAtNextLogin', name: 'changePasswordAtNextLogin',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether the user is forced to change their password at next login', description: 'Whether the user is forced to change their password at next login',
@@ -214,8 +363,7 @@ export const userFields: INodeProperties[] = [
value: 'home_fax', value: 'home_fax',
}, },
{ {
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased name: 'ISDN',
name: 'isdn',
value: 'isdn', value: 'isdn',
}, },
{ {
@@ -247,8 +395,7 @@ export const userFields: INodeProperties[] = [
value: 'telex', value: 'telex',
}, },
{ {
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased name: 'TTY TDD',
name: 'tty tdd',
value: 'tty_tdd', value: 'tty_tdd',
}, },
{ {
@@ -282,8 +429,7 @@ export const userFields: INodeProperties[] = [
name: 'primary', name: 'primary',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: description: "Whether this is the user's primary phone number",
"Whether this is the user's primary phone number. A user may only have one primary phone number.",
}, },
], ],
}, },
@@ -334,94 +480,181 @@ export const userFields: INodeProperties[] = [
}, },
], ],
}, },
],
},
/* -------------------------------------------------------------------------- */
/* user:delete */
/* -------------------------------------------------------------------------- */
{ {
displayName: 'User ID', displayName: 'Roles',
name: 'userId', name: 'roles',
type: 'string', type: 'multiOptions',
required: true, default: [],
displayOptions: { description: 'Select the roles you want to assign to the user',
show: { options: rolesOptions,
operation: ['delete'],
resource: ['user'],
}, },
{
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: '', default: '',
description: description:
"The value can be the user's primary email address, alias email address, or unique user ID", 'Select the schema to use for custom fields. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
}, },
{
displayName: 'Field Name or ID',
name: 'fieldName',
type: 'string',
default: '',
required: true,
description: 'Enter a field name from the selected schema',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
description: 'Provide a value for the selected field',
},
],
},
],
},
],
},
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* user:get */ /* user:get */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'User ID', displayName: 'Output',
name: 'userId', name: 'output',
type: 'string', type: 'options',
required: true, required: true,
default: 'simplified',
displayOptions: { displayOptions: {
show: { show: {
operation: ['get'], operation: ['get'],
resource: ['user'], resource: ['user'],
}, },
}, },
default: '', options: [
{
name: 'Simplified',
value: 'simplified',
description: description:
"The value can be the user's primary email address, alias email address, or unique user ID", 'Only return specific fields: kind, ID, primaryEmail, name (with subfields), isAdmin, lastLoginTime, creationTime, and suspended',
}, },
{ {
displayName: 'Projection', 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: 'Creation Time',
value: 'creationTime',
},
{
name: 'Is Admin',
value: 'isAdmin',
},
{
name: 'Kind',
value: 'kind',
},
{
name: 'Last Login Time',
value: 'lastLoginTime',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Primary Email',
value: 'primaryEmail',
},
{
name: 'Suspended',
value: 'suspended',
},
],
description: 'Fields to include in the response when "Select Included Fields" is chosen',
},
{
displayName: 'Custom Fields',
name: 'projection', name: 'projection',
type: 'options', type: 'options',
required: true, required: true,
options: [ options: [
{ {
name: 'Basic', name: "Don't Include",
value: 'basic', value: 'basic',
description: 'Do not include any custom fields for the user', description: 'Do not include any custom fields for the user',
}, },
{ {
name: 'Custom', name: 'Custom',
value: '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', value: 'full',
description: 'Include all fields associated with this user', description: 'Include all fields associated with this user',
}, },
], ],
default: 'basic',
displayOptions: { displayOptions: {
show: { show: {
operation: ['get'], operation: ['get'],
resource: ['user'], resource: ['user'],
}, },
}, },
default: 'basic',
description: 'What subset of fields to fetch for this user', description: 'What subset of fields to fetch for this user',
}, },
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
operation: ['get'],
resource: ['user'],
},
},
options: [
{ {
displayName: 'Custom Schema Names or IDs', displayName: 'Custom Schema Names or IDs',
name: 'customFieldMask', name: 'customFieldMask',
type: 'multiOptions', type: 'multiOptions',
required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: ['get'],
resource: ['user'],
'/projection': ['custom'], '/projection': ['custom'],
}, },
}, },
@@ -430,32 +663,9 @@ export const userFields: INodeProperties[] = [
}, },
default: [], default: [],
description: 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 <a href="https://docs.n8n.io/code/expressions/">expression</a>.', 'A comma-separated list of schema names. All fields from these schemas are fetched. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
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.',
},
],
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* user:getAll */ /* user:getAll */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@@ -491,41 +701,135 @@ export const userFields: INodeProperties[] = [
description: 'Max number of results to return', description: 'Max number of results to return',
}, },
{ {
displayName: 'Projection', displayName: 'Output',
name: 'projection', name: 'output',
type: 'options', type: 'options',
required: true, required: true,
options: [ default: 'simplified',
{
name: 'Basic',
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',
},
{
name: 'Full',
value: 'full',
description: 'Include all fields associated with this user',
},
],
displayOptions: { displayOptions: {
show: { show: {
operation: ['getAll'], operation: ['getAll'],
resource: ['user'], 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: 'Creation Time',
value: 'creationTime',
},
{
name: 'Is Admin',
value: 'isAdmin',
},
{
name: 'Kind',
value: 'kind',
},
{
name: 'Last Login Time',
value: 'lastLoginTime',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Primary Email',
value: 'primaryEmail',
},
{
name: 'Suspended',
value: 'suspended',
},
],
description: 'Fields to include in the response when "Select Included Fields" is chosen',
},
{
displayName: 'Custom Fields',
name: 'projection',
type: 'options',
required: true,
displayOptions: {
show: {
operation: ['getAll'],
resource: ['user'],
},
},
options: [
{
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 Custom Schema Names or IDs',
},
{
name: 'Include All',
value: 'full',
description: 'Include all fields associated with this user',
},
],
default: 'basic', default: 'basic',
description: 'What subset of fields to fetch for this user', description: 'What subset of fields to fetch for this user',
}, },
{ {
displayName: 'Options', displayName: 'Custom Schema Names or IDs',
name: 'options', name: 'customFieldMask',
type: 'multiOptions',
required: true,
displayOptions: {
show: {
operation: ['getAll'],
resource: ['user'],
'/projection': ['custom'],
},
},
typeOptions: {
loadOptionsMethod: 'getSchemas',
},
default: [],
description:
'A comma-separated list of schema names. All fields from these schemas are fetched. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Filter',
name: 'filter',
type: 'collection', type: 'collection',
placeholder: 'Add option', placeholder: 'Add Filter',
default: {}, default: {},
displayOptions: { displayOptions: {
show: { show: {
@@ -534,37 +838,55 @@ export const userFields: INodeProperties[] = [
}, },
}, },
options: [ 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 <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{ {
displayName: 'Customer', displayName: 'Customer',
name: 'customer', name: 'customer',
type: 'string', type: 'string',
default: '', default: '',
description: description: "The unique ID for the customer's Google Workspace account",
"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.",
}, },
{ {
displayName: 'Domain', displayName: 'Domain',
name: 'domain', name: 'domain',
type: 'string', type: 'string',
default: '', 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: 'Query',
name: 'query',
type: 'string',
placeholder: 'e.g. name:contact* email:contact*',
default: '',
description:
'Query string to filter the results. Follow Google Admin SDK documentation. <a href="https://developers.google.com/admin-sdk/directory/v1/guides/search-users#examples" target="_blank">More info</a>.',
},
{
displayName: 'Show Deleted',
name: 'showDeleted',
type: 'boolean',
default: false,
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: [
{
name: 'sortRules',
displayName: 'Sort Rules',
values: [
{ {
displayName: 'Order By', displayName: 'Order By',
name: 'orderBy', name: 'orderBy',
@@ -584,22 +906,7 @@ export const userFields: INodeProperties[] = [
}, },
], ],
default: '', default: '',
description: 'Property to use for sorting results', description: 'Field to sort the results by',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
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 <a href="https://developers.google.com/admin-sdk/directory/v1/guides/search-users">Search for Users</a>.',
},
{
displayName: 'Show Deleted',
name: 'showDeleted',
type: 'boolean',
default: false,
description: 'Whether to retrieve the list of deleted users',
}, },
{ {
displayName: 'Sort Order', displayName: 'Sort Order',
@@ -615,51 +922,18 @@ export const userFields: INodeProperties[] = [
value: 'DESCENDING', value: 'DESCENDING',
}, },
], ],
default: '', default: 'ASCENDING',
description: 'Whether to return results in ascending or descending order', description: 'Sort order direction',
},
{
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.',
}, },
], ],
}, },
],
description: 'Define sorting rules for the results',
},
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* user:update */ /* user:update */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'userId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['update'],
resource: ['user'],
},
},
default: '',
description:
"The value can be the user's primary email address, alias email address, or unique user ID",
},
{ {
displayName: 'Update Fields', displayName: 'Update Fields',
name: 'updateFields', name: 'updateFields',
@@ -681,9 +955,16 @@ export const userFields: INodeProperties[] = [
description: 'Whether user is archived', 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', name: 'changePasswordAtNextLogin',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether the user is forced to change their password at next login', description: 'Whether the user is forced to change their password at next login',
@@ -693,12 +974,14 @@ export const userFields: INodeProperties[] = [
name: 'firstName', name: 'firstName',
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'e.g. John',
}, },
{ {
displayName: 'Last Name', displayName: 'Last Name',
name: 'lastName', name: 'lastName',
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'e.g. Doe',
}, },
{ {
displayName: 'Password', displayName: 'Password',
@@ -706,6 +989,7 @@ export const userFields: INodeProperties[] = [
type: 'string', type: 'string',
typeOptions: { password: true }, typeOptions: { password: true },
default: '', default: '',
placeholder: 'e.g. MyStrongP@ssword123',
description: description:
'Stores the password for the user account. A minimum of 8 characters is required. The maximum length is 100 characters.', 'Stores the password for the user account. A minimum of 8 characters is required. The maximum length is 100 characters.',
}, },
@@ -761,7 +1045,7 @@ export const userFields: INodeProperties[] = [
value: 'home_fax', value: 'home_fax',
}, },
{ {
name: 'Isdn', name: 'ISDN',
value: 'isdn', value: 'isdn',
}, },
{ {
@@ -793,7 +1077,7 @@ export const userFields: INodeProperties[] = [
value: 'telex', value: 'telex',
}, },
{ {
name: 'Tty Tdd', name: 'TTY TDD',
value: 'tty_tdd', value: 'tty_tdd',
}, },
{ {
@@ -821,6 +1105,7 @@ export const userFields: INodeProperties[] = [
name: 'value', name: 'value',
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'e.g. +1234567890',
}, },
{ {
displayName: 'Primary', displayName: 'Primary',
@@ -839,6 +1124,7 @@ export const userFields: INodeProperties[] = [
name: 'primaryEmail', name: 'primaryEmail',
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'e.g. john.doe@example.com',
description: 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.", "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 +1168,61 @@ export const userFields: INodeProperties[] = [
name: 'address', name: 'address',
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'e.g. john.doe.work@example.com',
},
],
},
],
},
{
displayName: 'Roles',
name: 'roles',
type: 'multiOptions',
default: [],
description: 'Select the roles you want to assign to the user',
options: rolesOptions,
},
{
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 <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Field Name or ID',
name: 'fieldName',
type: 'string',
default: '',
required: true,
description: 'Enter a field name from the selected schema',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
description: 'Provide a value for the selected field',
}, },
], ],
}, },

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 192 192" height="192" viewBox="0 0 192 192" width="192"><rect fill="none" height="192" width="192"/><g><g><path d="M24.53,130l20.69,35.84c3.27,5.67,9.32,9.16,15.86,9.16h44.06l-7.93-24.07L78.15,130l-26.81-4.66 L24.53,130z" fill="#1967D2"/><path d="M182.06,86.84l-20.09-34.8l-17.28,19.13L134.49,96l11.08,26.57l16.27,17.6l20.22-35.01 C185.33,99.49,185.33,92.51,182.06,86.84z" fill="#1967D2"/><path d="M105.15,17H61.09c-6.54,0-12.59,3.49-15.86,9.16L24.53,62l26.81,4.66L78.15,62l19.57-20.93L105.15,17z" fill="#1967D2"/><path d="M57.75,96l20.4-34H24.53L10.19,86.84c-3.27,5.67-3.27,12.65,0,18.32L24.53,130h53.62L57.75,96z" fill="#4285F4"/><path d="M114.09,62l20.4,34l27.47-43.96l-14.94-25.88c-3.27-5.67-9.32-9.16-15.86-9.16h-26.01l-27,45H114.09z" fill="#4285F4"/><path d="M114.09,130H78.15l27,45h26.01c6.54,0,12.59-3.49,15.86-9.16l14.82-25.67L134.49,96L114.09,130z" fill="#4285F4"/></g></g></svg>

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,538 @@
import type { ILoadOptionsFunctions, IExecuteFunctions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import nock from 'nock';
import { returnData } from '../../../E2eTest/mock';
import { googleApiRequest, googleApiRequestAllItems } from '../GenericFunctions';
import { GSuiteAdmin } from '../GSuiteAdmin.node';
jest.mock('../GenericFunctions', () => ({
getGoogleAuth: jest.fn().mockImplementation(() => ({
oauth2Client: {
setCredentials: jest.fn(),
getAccessToken: jest.fn().mockResolvedValue('mock-access-token'),
},
})),
googleApiRequest: jest.fn(),
googleApiRequestAllItems: jest.fn(),
}));
const node = new GSuiteAdmin();
const mockThis = {
getNode: () => ({
name: 'Google Workspace Admin',
parameters: {},
}),
helpers: {
httpRequestWithAuthentication: jest.fn(),
returnJsonArray: (data: any) => data,
constructExecutionMetaData: (data: any) => data,
},
continueOnFail: () => false,
getNodeParameter: jest.fn((name: string) => {
if (name === 'limit') return 50;
return undefined;
}),
} as unknown as ILoadOptionsFunctions;
describe('GSuiteAdmin Node - loadOptions', () => {
beforeEach(() => {
jest.clearAllMocks();
nock.cleanAll();
nock.disableNetConnect();
});
describe('getDomains', () => {
it('should return a list of domains', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValue([
{ domainName: 'example.com' },
{ domainName: 'test.com' },
]);
const result = await node.methods.loadOptions.getDomains.call(mockThis);
expect(result).toEqual([
{ name: 'example.com', value: 'example.com' },
{ name: 'test.com', value: 'test.com' },
]);
});
});
describe('getSchemas', () => {
it('should return a list of schemas', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValue([
{ displayName: 'Employee Info', schemaName: 'EmployeeSchema' },
{ displayName: '', schemaName: 'CustomSchema' },
]);
const result = await node.methods.loadOptions.getSchemas.call(mockThis);
expect(result).toEqual([
{ name: 'Employee Info', value: 'EmployeeSchema' },
{ name: 'CustomSchema', value: 'CustomSchema' },
]);
});
it('should correctly iterate over schemas and return expected values', async () => {
const schemas = [
{ displayName: 'Employee Info', schemaName: 'EmployeeSchema' },
{ displayName: 'Custom Schema', schemaName: 'CustomSchema' },
];
const result = schemas.map((schema) => ({
name: schema.displayName,
value: schema.schemaName,
}));
expect(result).toEqual([
{ name: 'Employee Info', value: 'EmployeeSchema' },
{ name: 'Custom Schema', value: 'CustomSchema' },
]);
});
});
describe('getOrgUnits', () => {
it('should return a list of organizational units', async () => {
(googleApiRequest as jest.Mock).mockResolvedValue({
organizationUnits: [
{ name: 'Engineering', orgUnitPath: '/engineering' },
{ name: 'HR', orgUnitPath: '/hr' },
],
});
const result = await node.methods.loadOptions.getOrgUnits.call(mockThis);
expect(result).toEqual([
{ name: 'Engineering', value: '/engineering' },
{ name: 'HR', value: '/hr' },
]);
});
});
});
describe('GSuiteAdmin Node - logic coverage', () => {
it('should apply all filters correctly into qs', () => {
const filter = {
customer: 'my_customer',
domain: 'example.com',
query: 'name:admin',
userId: 'user@example.com',
showDeleted: true,
};
const sort = {
sortRules: { orderBy: 'email', sortOrder: 'ASCENDING' },
};
const qs: Record<string, any> = {};
if (filter.customer) qs.customer = filter.customer;
if (filter.domain) qs.domain = filter.domain;
if (filter.query) {
const query = filter.query.trim();
const regex = /^(name|email):\S+$/;
if (!regex.test(query)) {
throw new NodeOperationError(
mockThis.getNode(),
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
}
qs.query = query;
}
if (filter.userId) qs.userId = filter.userId;
if (filter.showDeleted) qs.showDeleted = 'true';
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) qs.orderBy = orderBy;
if (sortOrder) qs.sortOrder = sortOrder;
}
expect(qs).toEqual({
customer: 'my_customer',
domain: 'example.com',
query: 'name:admin',
userId: 'user@example.com',
showDeleted: 'true',
orderBy: 'email',
sortOrder: 'ASCENDING',
});
});
it('should throw an error for invalid query format', () => {
const filter = {
query: 'invalidQuery',
};
const qs: Record<string, any> = {};
expect(() => {
if (filter.query) {
const query = filter.query.trim();
const regex = /^(name|email):\S+$/;
if (!regex.test(query)) {
throw new NodeOperationError(
mockThis.getNode(),
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
}
qs.query = query;
}
}).toThrow(
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
});
it('should assign my_customer when customer is not defined', () => {
const qs: Record<string, any> = {};
if (!qs.customer) qs.customer = 'my_customer';
expect(qs.customer).toBe('my_customer');
});
it('should throw an error if username is empty', () => {
const mock = { getNode: () => ({}) } as IExecuteFunctions;
expect(() => {
const username = '';
if (!username) {
throw new NodeOperationError(mock.getNode(), "The parameter 'Username' is empty", {
itemIndex: 0,
description: "Please fill in the 'Username' parameter to create the user",
});
}
}).toThrow("The parameter 'Username' is empty");
});
it('should set phones, emails, roles, and custom fields', () => {
const additionalFields = {
phoneUi: { phoneValues: [{ type: 'work', value: '123' }] },
emailUi: { emailValues: [{ address: 'test@example.com', type: 'home' }] },
roles: ['superAdmin', 'groupsAdmin'],
customFields: {
fieldValues: [{ schemaName: 'CustomSchema', fieldName: 'customField', value: 'abc' }],
},
};
const body: Record<string, any> = {};
if (additionalFields.phoneUi) {
body.phones = additionalFields.phoneUi.phoneValues;
}
if (additionalFields.emailUi) {
body.emails = additionalFields.emailUi.emailValues;
}
if (additionalFields.roles) {
const roles = additionalFields.roles;
body.roles = {
superAdmin: roles.includes('superAdmin'),
groupsAdmin: roles.includes('groupsAdmin'),
groupsReader: false,
groupsEditor: false,
userManagement: false,
helpDeskAdmin: false,
servicesAdmin: false,
inventoryReportingAdmin: false,
storageAdmin: false,
directorySyncAdmin: false,
mobileAdmin: false,
};
}
if (additionalFields.customFields) {
const customSchemas: Record<string, any> = {};
for (const field of additionalFields.customFields.fieldValues) {
if (
!field.schemaName ||
!field.fieldName ||
field.value === undefined ||
field.value === ''
) {
continue;
}
if (!customSchemas[field.schemaName]) customSchemas[field.schemaName] = {};
customSchemas[field.schemaName][field.fieldName] = field.value;
}
if (Object.keys(customSchemas).length > 0) {
body.customSchemas = customSchemas;
}
}
expect(body).toEqual({
phones: [{ type: 'work', value: '123' }],
emails: [{ address: 'test@example.com', type: 'home' }],
roles: {
superAdmin: true,
groupsAdmin: true,
groupsReader: false,
groupsEditor: false,
userManagement: false,
helpDeskAdmin: false,
servicesAdmin: false,
inventoryReportingAdmin: false,
storageAdmin: false,
directorySyncAdmin: false,
mobileAdmin: false,
},
customSchemas: {
CustomSchema: { customField: 'abc' },
},
});
});
it('should set customFieldMask and fields if projection is custom and output is select', () => {
const projection = 'custom';
const output = 'select';
const fields = ['primaryEmail'];
const qs: Record<string, any> = {
customFieldMask: ['Custom1', 'Custom2'],
};
if (projection === 'custom' && qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(',');
}
if (output === 'select') {
if (!fields.includes('id')) fields.push('id');
qs.fields = fields.join(',');
}
expect(qs).toEqual({
customFieldMask: 'Custom1,Custom2',
fields: 'primaryEmail,id',
});
});
it('should set fields for user getAll when returnAll is false', () => {
const qs: Record<string, any> = {};
const returnAll = false;
const fields = ['primaryEmail'];
const output = 'select';
const projection = 'custom';
qs.customFieldMask = ['Custom1', 'Custom2'];
if (projection === 'custom' && 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) qs.maxResults = 50;
expect(qs).toEqual({
customFieldMask: 'Custom1,Custom2',
fields: 'users(primaryEmail,id)',
customer: 'my_customer',
maxResults: 50,
});
});
});
describe('GSuiteAdmin Node - user:update logic', () => {
it('should build suspended, roles, and customSchemas', async () => {
const mockCall = jest.fn().mockResolvedValue([{ success: true }]);
(googleApiRequest as jest.Mock).mockImplementation(mockCall);
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
getNodeParameter: jest.fn((paramName: string) => {
switch (paramName) {
case 'resource':
return 'user';
case 'operation':
return 'update';
case 'userId':
return 'user-id-123';
case 'updateFields':
return {
suspendUi: true,
roles: ['superAdmin', 'groupsReader'],
customFields: {
fieldValues: [
{ schemaName: 'CustomSchema1', fieldName: 'fieldA', value: 'valueA' },
{ schemaName: 'CustomSchema1', fieldName: 'fieldB', value: 'valueB' },
{ schemaName: 'CustomSchema2', fieldName: 'fieldX', value: 'valueX' },
],
},
};
default:
return undefined;
}
}),
helpers: {
returnJsonArray: (data: any) => data,
constructExecutionMetaData: (data: any) => data,
},
continueOnFail: () => false,
getInputData: () => [{ json: {} }],
} as unknown as IExecuteFunctions;
await new GSuiteAdmin().execute.call(mockContext);
const calledBody = mockCall.mock.calls[0][2];
expect(calledBody.suspended).toBe(true);
expect(calledBody.roles).toEqual({
superAdmin: true,
groupsAdmin: false,
groupsReader: true,
groupsEditor: false,
userManagement: false,
helpDeskAdmin: false,
servicesAdmin: false,
inventoryReportingAdmin: false,
storageAdmin: false,
directorySyncAdmin: false,
mobileAdmin: false,
});
expect(calledBody.customSchemas).toEqual({
CustomSchema1: {
fieldA: 'valueA',
fieldB: 'valueB',
},
CustomSchema2: {
fieldX: 'valueX',
},
});
});
it('should throw error for invalid custom fields', async () => {
const mockCall = jest.fn();
(googleApiRequest as jest.Mock).mockImplementation(mockCall);
const mockContextInvalidFields = {
getNode: () => ({ name: 'GSuiteAdmin' }),
getNodeParameter: jest.fn((paramName: string) => {
switch (paramName) {
case 'resource':
return 'user';
case 'operation':
return 'update';
case 'userId':
return 'user-id-456';
case 'updateFields':
return {
customFields: {
fieldValues: [
{ schemaName: '', fieldName: 'valid', value: 'ok' },
{ schemaName: 'ValidSchema', fieldName: 'valid', value: 'ok' },
],
},
};
default:
return undefined;
}
}),
helpers: {
returnJsonArray: (data: any) => data,
constructExecutionMetaData: (data: any) => data,
},
continueOnFail: () => false,
getInputData: () => [{ json: {} }],
} as unknown as IExecuteFunctions;
await expect(new GSuiteAdmin().execute.call(mockContextInvalidFields)).rejects.toThrow(
'Invalid custom field data',
);
expect(mockCall).not.toHaveBeenCalled();
});
it('should throw an error if username is empty', () => {
const mock = { getNode: () => ({}) } as IExecuteFunctions;
expect(() => {
const username = '';
if (!username) {
throw new NodeOperationError(mock.getNode(), "The parameter 'Username' is empty", {
itemIndex: 0,
description: "Please fill in the 'Username' parameter to create the user",
});
}
}).toThrow("The parameter 'Username' is empty");
});
});
describe('GSuiteAdmin Node - Error Handling', () => {
it('should throw a NodeOperationError if the error is an instance of NodeOperationError', async () => {
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
continueOnFail: () => false,
helpers: {
constructExecutionMetaData: jest.fn(),
returnJsonArray: jest.fn(),
},
} as unknown as IExecuteFunctions;
const error = new NodeOperationError(mockContext.getNode(), 'Some error message');
await expect(async () => {
throw error;
}).rejects.toThrow(NodeOperationError);
});
it('should handle error when continueOnFail is true and constructExecutionMetaData is called', async () => {
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
continueOnFail: () => true,
helpers: {
constructExecutionMetaData: jest.fn().mockReturnValue([{ message: 'mock error data' }]),
returnJsonArray: jest.fn().mockReturnValue([]),
},
} as unknown as IExecuteFunctions;
const error = new Error('Some error message');
await expect(async () => {
if (error instanceof NodeOperationError) {
throw error;
}
if (mockContext.continueOnFail()) {
const executionErrorData = mockContext.helpers.constructExecutionMetaData(
mockContext.helpers.returnJsonArray({
message: 'Operation "update" failed for resource "user".',
description: error.message,
}),
{ itemData: { item: 0 } },
);
if (executionErrorData) {
returnData.push(...executionErrorData);
} else {
console.error('executionErrorData is not iterable:', executionErrorData);
}
}
throw new NodeOperationError(
mockContext.getNode(),
'Operation "update" failed for resource "user".',
{
description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.message}`,
itemIndex: 0,
},
);
}).rejects.toThrow(NodeOperationError);
});
it('should throw a NodeOperationError if an unknown error is thrown and continueOnFail is false', async () => {
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
continueOnFail: () => false,
helpers: {
constructExecutionMetaData: jest.fn(),
returnJsonArray: jest.fn(),
},
} as unknown as IExecuteFunctions;
const error = new Error('Some unknown error');
await expect(async () => {
if (error instanceof NodeOperationError) {
throw error;
}
if (!mockContext.continueOnFail()) {
throw new NodeOperationError(
mockContext.getNode(),
'Operation "update" failed for resource "user".',
{
description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.message}`,
itemIndex: 0,
},
);
}
}).rejects.toThrow(NodeOperationError);
});
});

View File

@@ -0,0 +1,173 @@
import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { googleApiRequest, googleApiRequestAllItems } from '../GenericFunctions';
describe('Google GSuiteAdmin Node', () => {
let mockContext: IExecuteFunctions | ILoadOptionsFunctions;
beforeEach(() => {
mockContext = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
},
getNode: jest.fn(),
} as unknown as IExecuteFunctions | ILoadOptionsFunctions;
jest.clearAllMocks();
});
it('should make a successful API request with default options', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
success: true,
});
const result = await googleApiRequest.call(mockContext, 'GET', '/example/resource');
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
url: 'https://www.googleapis.com/admin/example/resource',
headers: { 'Content-Type': 'application/json' },
json: true,
qs: {},
}),
);
expect(result).toEqual({ success: true });
});
it('should omit the body if it is empty', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
success: true,
});
await googleApiRequest.call(mockContext, 'GET', '/example/resource', {});
expect(mockContext.helpers.httpRequestWithAuthentication).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.httpRequestWithAuthentication as jest.Mock).mockRejectedValueOnce(
errorResponse,
);
await expect(googleApiRequest.call(mockContext, 'GET', '/example/resource')).rejects.toThrow(
NodeApiError,
);
expect(mockContext.getNode).toHaveBeenCalled();
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalled();
});
it('should return all items across multiple pages', async () => {
(mockContext.helpers.httpRequestWithAuthentication 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,
'items',
'GET',
'/example/resource',
);
expect(result).toEqual([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }]);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(3);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
1,
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: { maxResults: 100, pageToken: '' },
headers: { 'Content-Type': 'application/json' },
url: 'https://www.googleapis.com/admin/example/resource',
json: true,
}),
);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
2,
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: { maxResults: 100, pageToken: '' },
headers: { 'Content-Type': 'application/json' },
url: 'https://www.googleapis.com/admin/example/resource',
json: true,
}),
);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
3,
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: { maxResults: 100, pageToken: '' },
headers: { 'Content-Type': 'application/json' },
url: 'https://www.googleapis.com/admin/example/resource',
json: true,
}),
);
});
it('should handle single-page responses', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
nextPageToken: '',
items: [{ id: '1' }, { id: '2' }],
});
const result = await googleApiRequestAllItems.call(
mockContext,
'items',
'GET',
'/example/resource',
);
expect(result).toEqual([{ id: '1' }, { id: '2' }]);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
});
it('should handle empty responses', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
nextPageToken: '',
items: [],
});
const result = await googleApiRequestAllItems.call(
mockContext,
'items',
'GET',
'/example/resource',
);
expect(result).toEqual([]);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
});
it('should throw a NodeApiError if a request fails', async () => {
const errorResponse = { message: 'API Error' };
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockRejectedValueOnce(
errorResponse,
);
await expect(
googleApiRequestAllItems.call(mockContext, 'items', 'GET', '/example/resource'),
).rejects.toThrow();
expect(mockContext.getNode).toHaveBeenCalled();
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,104 @@
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import { googleApiRequest, googleApiRequestAllItems } from '../GenericFunctions';
import { searchUsers, searchGroups, searchDevices } from '../SearchFunctions';
jest.mock('../GenericFunctions');
describe('searchFunctions', () => {
let mockContext: ILoadOptionsFunctions;
beforeEach(() => {
mockContext = {
getNodeParameter: jest.fn(),
} as unknown as ILoadOptionsFunctions;
jest.clearAllMocks();
});
describe('searchUsers', () => {
it('should return formatted user search results', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([
{ id: '123', name: { fullName: 'John Doe' } },
{ id: '456' },
]);
const result = await searchUsers.call(mockContext);
expect(googleApiRequestAllItems).toHaveBeenCalledWith(
expect.anything(),
'GET',
'/directory/v1/users',
{},
{ customer: 'my_customer' },
);
expect(result).toEqual({
results: [
{ name: 'John Doe', value: '123' },
{ name: '456', value: '456' },
],
});
});
it('should return an empty array if no users found', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]);
const result = await searchUsers.call(mockContext);
expect(result).toEqual({ results: [] });
});
});
describe('searchGroups', () => {
it('should return formatted group search results', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([
{ id: 'group1', name: 'Group One' },
{ id: 'group2', email: 'group@example.com' },
{ id: 'group3' },
]);
const result = await searchGroups.call(mockContext);
expect(result).toEqual({
results: [
{ name: 'Group One', value: 'group1' },
{ name: 'group@example.com', value: 'group2' },
{ name: 'Unnamed Group', value: 'group3' },
],
});
});
it('should return empty results if no groups found', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]);
const result = await searchGroups.call(mockContext);
expect(result).toEqual({ results: [] });
});
});
describe('searchDevices', () => {
it('should return formatted device search results', async () => {
(googleApiRequest as jest.Mock).mockResolvedValueOnce({
chromeosdevices: [{ deviceId: 'dev1', serialNumber: 'SN123' }, { deviceId: 'Dev2' }],
});
const result = await searchDevices.call(mockContext);
expect(googleApiRequest).toHaveBeenCalledWith(
'GET',
'/directory/v1/customer/my_customer/devices/chromeos/',
{},
{ customerId: 'my_customer' },
);
expect(result).toEqual({
results: [
{ name: 'SN123', value: 'dev1' },
{ name: 'Dev2', value: 'Dev2' },
],
});
});
it('should return empty results if no devices found', async () => {
(googleApiRequest as jest.Mock).mockResolvedValueOnce({ chromeosdevices: [] });
const result = await searchDevices.call(mockContext);
expect(result).toEqual({ results: [] });
});
});
});

View File

@@ -0,0 +1,31 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('changeStatus.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.post(
'/directory/v1/customer/my_customer/devices/chromeos/9140fcff-7ba7-4324-8552-f7de68481b4c/action',
{
action: 'reenable',
},
)
.reply(200, {
kind: 'admin#directory#chromeosdeviceAction',
action: 'reenable',
status: 'SUCCESS',
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,59 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-180, 240],
"id": "db450654-d59c-4fb4-a06c-af7b971f0c14",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"operation": "changeStatus",
"deviceId": {
"__rl": true,
"value": "9140fcff-7ba7-4324-8552-f7de68481b4c",
"mode": "list",
"cachedResultName": "5CC115NN33"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [-320, 680],
"id": "d441de38-e340-495d-8177-f8bd8bb33e50",
"name": "Change Status",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Change Status",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Change Status": [
{
"json": {
"kind": "admin#directory#chromeosdeviceAction",
"action": "reenable",
"status": "SUCCESS"
}
}
]
}
}

View File

@@ -0,0 +1,43 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('get.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get(
'/directory/v1/customer/my_customer/devices/chromeos/9999ffff-7aa7-4444-8555-f7de48484b4a?projection=basic',
)
.reply(200, {
kind: 'admin#directory#chromeosdevice',
etag: '"example"',
deviceId: '9999ffff-7aa7-4444-8555-f7de48484b4a',
serialNumber: '5DD1155DD44',
status: 'DISABLED',
lastSync: '2025-02-12T07:17:16.950Z',
annotatedUser: 'my user',
annotatedLocation: 'test',
annotatedAssetId: '1234567788',
notes: 'test',
orgUnitPath: '/',
orgUnitId: '00pp8a2z1uu85pp',
extendedSupportEligible: false,
chromeOsType: 'chromeOs',
diskSpaceUsage: {
capacityBytes: '549755813888',
usedBytes: '549755813888',
},
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,73 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0ffead0b-d690-48b8-b406-bea5e0029c15",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"deviceId": {
"__rl": true,
"value": "9999ffff-7aa7-4444-8555-f7de48484b4a",
"mode": "list",
"cachedResultName": "5DD1155DD44"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [0, 1120],
"id": "cda57e63-1620-4f75-b11e-48b83565ad80",
"name": "Get Device",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Device",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Device": [
{
"json": {
"kind": "admin#directory#chromeosdevice",
"etag": "\"example\"",
"deviceId": "9999ffff-7aa7-4444-8555-f7de48484b4a",
"serialNumber": "5DD1155DD44",
"status": "DISABLED",
"lastSync": "2025-02-12T07:17:16.950Z",
"annotatedUser": "my user",
"annotatedLocation": "test",
"annotatedAssetId": "1234567788",
"notes": "test",
"orgUnitPath": "/",
"orgUnitId": "00pp8a2z1uu85pp",
"extendedSupportEligible": false,
"chromeOsType": "chromeOs",
"diskSpaceUsage": {
"capacityBytes": "549755813888",
"usedBytes": "549755813888"
}
}
}
]
}
}

View File

@@ -0,0 +1,35 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('getAll.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/customer/my_customer/devices/chromeos/')
.query({
customer: 'my_customer',
includeChildOrgunits: false,
maxResults: 100,
orderBy: 'notes',
orgUnitPath: '/admin-google Testing OU/Child OU',
projection: 'basic',
})
.reply(200, [
{
kind: 'admin#directory#chromeosdevices',
etag: '"example"',
},
]);
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,61 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"operation": "getAll",
"filter": {
"orgUnitPath": "/admin-google Testing OU/Child OU"
},
"sort": {
"sortRules": {
"orderBy": "notes",
"sortBy": "ascending"
}
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [40, 1120],
"id": "b8a51950-2fdb-4161-9dc3-09f73de5a45b",
"name": "Get Many Device",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Many Device",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Many Device": [
{
"json": {
"kind": "admin#directory#chromeosdevices",
"etag": "\"example\""
}
}
]
}
}

View File

@@ -0,0 +1,627 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('update.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.put(
'/directory/v1/customer/my_customer/devices/chromeos/9990fpff-8ba8-4444-8555-f7ee88881b4c',
)
.reply(200, {
kind: 'admin#directory#chromeosdevice',
etag: '"example"',
deviceId: '9990fpff-8ba8-4444-8555-f7ee88881b4c',
serialNumber: '5CC115NN33',
status: 'DISABLED',
lastSync: '2025-02-12T07:17:16.950Z',
annotatedUser: 'my user',
annotatedLocation: 'test',
annotatedAssetId: '1234567788',
notes: 'test',
model: 'Test Model',
osVersion: '129.0.6668.99',
platformVersion: '16002.51.0 (Official Build) stable-channel reven',
firmwareVersion: 'FirmwareNotParsed',
macAddress: '666c8888ffccf',
lastEnrollmentTime: '2025-02-10T17:03:10.324Z',
firstEnrollmentTime: '2025-02-10T17:03:10.324Z',
orgUnitPath: '/',
orgUnitId: '00pp88a2z2uu88pp',
recentUsers: [
{
type: 'USER_TYPE_MANAGED',
email: 'admin-google@example.com',
},
],
activeTimeRanges: [
{
date: '2025-02-10',
activeTime: 300000,
},
{
date: '2025-02-11',
activeTime: 1920025,
},
{
date: '2025-02-12',
activeTime: 30000,
},
],
tpmVersionInfo: {
family: '0',
specLevel: '0',
manufacturer: '0',
tpmModel: '0',
firmwareVersion: '0',
vendorSpecific: '',
},
cpuStatusReports: [
{
reportTime: '2025-02-10T17:03:13.233Z',
cpuUtilizationPercentageInfo: [12],
},
{
reportTime: '2025-02-10T17:04:13.233Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 42,
label: 'Tctl\n',
},
{
temperature: 43,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T17:11:01.943Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-10T17:12:02.223Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 51,
label: 'Tctl\n',
},
{
temperature: 43,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T17:47:32.621Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-10T17:48:42.770Z',
cpuTemperatureInfo: [
{
temperature: 43,
label: 'edge\n',
},
{
temperature: 44,
label: 'Tctl\n',
},
{
temperature: 44,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T18:56:24.294Z',
cpuUtilizationPercentageInfo: [29],
},
{
reportTime: '2025-02-10T18:57:27.841Z',
cpuTemperatureInfo: [
{
temperature: 34,
label: 'edge\n',
},
{
temperature: 35,
label: 'Tctl\n',
},
{
temperature: 35,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T23:04:56.582Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-10T23:05:56.563Z',
cpuTemperatureInfo: [
{
temperature: 27,
label: 'edge\n',
},
{
temperature: 28,
label: 'Tctl\n',
},
{
temperature: 27,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T23:56:47.138Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-10T23:57:50.717Z',
cpuTemperatureInfo: [
{
temperature: 39,
label: 'edge\n',
},
{
temperature: 39,
label: 'Tctl\n',
},
{
temperature: 40,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T07:49:44.333Z',
cpuUtilizationPercentageInfo: [26],
},
{
reportTime: '2025-02-11T07:50:48.473Z',
cpuTemperatureInfo: [
{
temperature: 26,
label: 'edge\n',
},
{
temperature: 27,
label: 'Tctl\n',
},
{
temperature: 27,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T15:46:23.530Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T15:47:22.723Z',
cpuTemperatureInfo: [
{
temperature: 27,
label: 'edge\n',
},
{
temperature: 28,
label: 'Tctl\n',
},
{
temperature: 27,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T15:52:40.368Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-11T15:53:41.233Z',
cpuTemperatureInfo: [
{
temperature: 34,
label: 'edge\n',
},
{
temperature: 35,
label: 'Tctl\n',
},
{
temperature: 35,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:06:07.349Z',
cpuUtilizationPercentageInfo: [30],
},
{
reportTime: '2025-02-11T16:07:07.921Z',
cpuTemperatureInfo: [
{
temperature: 39,
label: 'edge\n',
},
{
temperature: 39,
label: 'Tctl\n',
},
{
temperature: 40,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:13:28.511Z',
cpuUtilizationPercentageInfo: [25],
},
{
reportTime: '2025-02-11T16:14:27.628Z',
cpuTemperatureInfo: [
{
temperature: 36,
label: 'edge\n',
},
{
temperature: 37,
label: 'Tctl\n',
},
{
temperature: 37,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:17:06.188Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T16:18:06.375Z',
cpuTemperatureInfo: [
{
temperature: 40,
label: 'edge\n',
},
{
temperature: 41,
label: 'Tctl\n',
},
{
temperature: 42,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:36:20.232Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T16:37:20.599Z',
cpuTemperatureInfo: [
{
temperature: 45,
label: 'edge\n',
},
{
temperature: 58,
label: 'Tctl\n',
},
{
temperature: 45,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:48:45.267Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T16:49:44.854Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 44,
label: 'Tctl\n',
},
{
temperature: 44,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-12T06:35:29.337Z',
cpuUtilizationPercentageInfo: [30],
},
{
reportTime: '2025-02-12T06:36:28.433Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 42,
label: 'Tctl\n',
},
{
temperature: 42,
label: 'acpitz\n',
},
],
},
],
systemRamTotal: '16089374720',
systemRamFreeReports: [
{
reportTime: '2025-02-10T17:03:13.230Z',
systemRamFreeInfo: ['13905453056'],
},
{
reportTime: '2025-02-10T17:11:01.697Z',
systemRamFreeInfo: ['15221055488'],
},
{
reportTime: '2025-02-10T17:47:32.153Z',
systemRamFreeInfo: ['15237283840'],
},
{
reportTime: '2025-02-10T18:56:23.878Z',
systemRamFreeInfo: ['15228760064'],
},
{
reportTime: '2025-02-10T23:04:56.127Z',
systemRamFreeInfo: ['15228022784'],
},
{
reportTime: '2025-02-10T23:56:46.839Z',
systemRamFreeInfo: ['15226499072'],
},
{
reportTime: '2025-02-11T07:49:43.939Z',
systemRamFreeInfo: ['15229087744'],
},
{
reportTime: '2025-02-11T15:46:23.165Z',
systemRamFreeInfo: ['15226187776'],
},
{
reportTime: '2025-02-11T15:52:39.966Z',
systemRamFreeInfo: ['15226843136'],
},
{
reportTime: '2025-02-11T16:06:06.871Z',
systemRamFreeInfo: ['15225753600'],
},
{
reportTime: '2025-02-11T16:13:28.176Z',
systemRamFreeInfo: ['15228182528'],
},
{
reportTime: '2025-02-11T16:17:05.936Z',
systemRamFreeInfo: ['15223095296'],
},
{
reportTime: '2025-02-11T16:36:19.897Z',
systemRamFreeInfo: ['15226126336'],
},
{
reportTime: '2025-02-11T16:48:44.934Z',
systemRamFreeInfo: ['15226707968'],
},
{
reportTime: '2025-02-12T06:35:28.949Z',
systemRamFreeInfo: ['15222706176'],
},
],
diskVolumeReports: [
{
volumeInfo: [
{
volumeId: '/media/archive',
storageTotal: '8044687360',
storageFree: '8044687360',
},
{
volumeId: '/media/removable',
storageTotal: '8044687360',
storageFree: '8044687360',
},
],
},
],
lastKnownNetwork: [
{
ipAddress: '192.168.0.106',
wanIpAddress: '87.121.13.137',
},
],
cpuInfo: [
{
model: 'AMD Ryzen 5 4500U with Radeon Graphics',
architecture: 'x64',
maxClockSpeedKhz: 2375000,
logicalCpus: [
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397253,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '59.509354s',
},
{
displayName: 'C1',
sessionDuration: '1.338153s',
},
{
displayName: 'C2',
sessionDuration: '0.241264s',
},
{
displayName: 'POLL',
sessionDuration: '0.004477s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397372,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '58.861175s',
},
{
displayName: 'C1',
sessionDuration: '1.335068s',
},
{
displayName: 'C2',
sessionDuration: '0.761853s',
},
{
displayName: 'POLL',
sessionDuration: '0.007583s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397454,
idleDuration: '58s',
cStates: [
{
displayName: 'C3',
sessionDuration: '57.457528s',
},
{
displayName: 'C1',
sessionDuration: '1.280076s',
},
{
displayName: 'C2',
sessionDuration: '0.167642s',
},
{
displayName: 'POLL',
sessionDuration: '0.003444s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397348,
idleDuration: '59s',
cStates: [
{
displayName: 'C3',
sessionDuration: '58.906343s',
},
{
displayName: 'C1',
sessionDuration: '1.101873s',
},
{
displayName: 'C2',
sessionDuration: '0.119013s',
},
{
displayName: 'POLL',
sessionDuration: '0.009095s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1383188,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '59.476621s',
},
{
displayName: 'C1',
sessionDuration: '1.048691s',
},
{
displayName: 'C2',
sessionDuration: '0.192808s',
},
{
displayName: 'POLL',
sessionDuration: '0.003546s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397437,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '60.155800s',
},
{
displayName: 'C1',
sessionDuration: '0.681644s',
},
{
displayName: 'C2',
sessionDuration: '0.143131s',
},
{
displayName: 'POLL',
sessionDuration: '0.004276s',
},
],
},
],
},
],
extendedSupportEligible: false,
chromeOsType: 'chromeOsFlex',
diskSpaceUsage: {
capacityBytes: '549755813888',
usedBytes: '85613068288',
},
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,661 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"operation": "update",
"deviceId": {
"__rl": true,
"value": "9990fpff-8ba8-4444-8555-f7ee88881b4c",
"mode": "list",
"cachedResultName": "5CC115NN33"
},
"updateOptions": {
"notes": "test"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [40, 1140],
"id": "52f7a4b5-7ab5-4bd1-b6eb-230341ab6057",
"name": "Update Device",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Update Device",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Update Device": [
{
"json": {
"kind": "admin#directory#chromeosdevice",
"etag": "\"example\"",
"deviceId": "9990fpff-8ba8-4444-8555-f7ee88881b4c",
"serialNumber": "5CC115NN33",
"status": "DISABLED",
"lastSync": "2025-02-12T07:17:16.950Z",
"annotatedUser": "my user",
"annotatedLocation": "test",
"annotatedAssetId": "1234567788",
"notes": "test",
"model": "Test Model",
"osVersion": "129.0.6668.99",
"platformVersion": "16002.51.0 (Official Build) stable-channel reven",
"firmwareVersion": "FirmwareNotParsed",
"macAddress": "666c8888ffccf",
"lastEnrollmentTime": "2025-02-10T17:03:10.324Z",
"firstEnrollmentTime": "2025-02-10T17:03:10.324Z",
"orgUnitPath": "/",
"orgUnitId": "00pp88a2z2uu88pp",
"recentUsers": [
{
"type": "USER_TYPE_MANAGED",
"email": "admin-google@example.com"
}
],
"activeTimeRanges": [
{
"date": "2025-02-10",
"activeTime": 300000
},
{
"date": "2025-02-11",
"activeTime": 1920025
},
{
"date": "2025-02-12",
"activeTime": 30000
}
],
"tpmVersionInfo": {
"family": "0",
"specLevel": "0",
"manufacturer": "0",
"tpmModel": "0",
"firmwareVersion": "0",
"vendorSpecific": ""
},
"cpuStatusReports": [
{
"reportTime": "2025-02-10T17:03:13.233Z",
"cpuUtilizationPercentageInfo": [12]
},
{
"reportTime": "2025-02-10T17:04:13.233Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 42,
"label": "Tctl\n"
},
{
"temperature": 43,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T17:11:01.943Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-10T17:12:02.223Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 51,
"label": "Tctl\n"
},
{
"temperature": 43,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T17:47:32.621Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-10T17:48:42.770Z",
"cpuTemperatureInfo": [
{
"temperature": 43,
"label": "edge\n"
},
{
"temperature": 44,
"label": "Tctl\n"
},
{
"temperature": 44,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T18:56:24.294Z",
"cpuUtilizationPercentageInfo": [29]
},
{
"reportTime": "2025-02-10T18:57:27.841Z",
"cpuTemperatureInfo": [
{
"temperature": 34,
"label": "edge\n"
},
{
"temperature": 35,
"label": "Tctl\n"
},
{
"temperature": 35,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T23:04:56.582Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-10T23:05:56.563Z",
"cpuTemperatureInfo": [
{
"temperature": 27,
"label": "edge\n"
},
{
"temperature": 28,
"label": "Tctl\n"
},
{
"temperature": 27,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T23:56:47.138Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-10T23:57:50.717Z",
"cpuTemperatureInfo": [
{
"temperature": 39,
"label": "edge\n"
},
{
"temperature": 39,
"label": "Tctl\n"
},
{
"temperature": 40,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T07:49:44.333Z",
"cpuUtilizationPercentageInfo": [26]
},
{
"reportTime": "2025-02-11T07:50:48.473Z",
"cpuTemperatureInfo": [
{
"temperature": 26,
"label": "edge\n"
},
{
"temperature": 27,
"label": "Tctl\n"
},
{
"temperature": 27,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T15:46:23.530Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T15:47:22.723Z",
"cpuTemperatureInfo": [
{
"temperature": 27,
"label": "edge\n"
},
{
"temperature": 28,
"label": "Tctl\n"
},
{
"temperature": 27,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T15:52:40.368Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-11T15:53:41.233Z",
"cpuTemperatureInfo": [
{
"temperature": 34,
"label": "edge\n"
},
{
"temperature": 35,
"label": "Tctl\n"
},
{
"temperature": 35,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:06:07.349Z",
"cpuUtilizationPercentageInfo": [30]
},
{
"reportTime": "2025-02-11T16:07:07.921Z",
"cpuTemperatureInfo": [
{
"temperature": 39,
"label": "edge\n"
},
{
"temperature": 39,
"label": "Tctl\n"
},
{
"temperature": 40,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:13:28.511Z",
"cpuUtilizationPercentageInfo": [25]
},
{
"reportTime": "2025-02-11T16:14:27.628Z",
"cpuTemperatureInfo": [
{
"temperature": 36,
"label": "edge\n"
},
{
"temperature": 37,
"label": "Tctl\n"
},
{
"temperature": 37,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:17:06.188Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T16:18:06.375Z",
"cpuTemperatureInfo": [
{
"temperature": 40,
"label": "edge\n"
},
{
"temperature": 41,
"label": "Tctl\n"
},
{
"temperature": 42,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:36:20.232Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T16:37:20.599Z",
"cpuTemperatureInfo": [
{
"temperature": 45,
"label": "edge\n"
},
{
"temperature": 58,
"label": "Tctl\n"
},
{
"temperature": 45,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:48:45.267Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T16:49:44.854Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 44,
"label": "Tctl\n"
},
{
"temperature": 44,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-12T06:35:29.337Z",
"cpuUtilizationPercentageInfo": [30]
},
{
"reportTime": "2025-02-12T06:36:28.433Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 42,
"label": "Tctl\n"
},
{
"temperature": 42,
"label": "acpitz\n"
}
]
}
],
"systemRamTotal": "16089374720",
"systemRamFreeReports": [
{
"reportTime": "2025-02-10T17:03:13.230Z",
"systemRamFreeInfo": ["13905453056"]
},
{
"reportTime": "2025-02-10T17:11:01.697Z",
"systemRamFreeInfo": ["15221055488"]
},
{
"reportTime": "2025-02-10T17:47:32.153Z",
"systemRamFreeInfo": ["15237283840"]
},
{
"reportTime": "2025-02-10T18:56:23.878Z",
"systemRamFreeInfo": ["15228760064"]
},
{
"reportTime": "2025-02-10T23:04:56.127Z",
"systemRamFreeInfo": ["15228022784"]
},
{
"reportTime": "2025-02-10T23:56:46.839Z",
"systemRamFreeInfo": ["15226499072"]
},
{
"reportTime": "2025-02-11T07:49:43.939Z",
"systemRamFreeInfo": ["15229087744"]
},
{
"reportTime": "2025-02-11T15:46:23.165Z",
"systemRamFreeInfo": ["15226187776"]
},
{
"reportTime": "2025-02-11T15:52:39.966Z",
"systemRamFreeInfo": ["15226843136"]
},
{
"reportTime": "2025-02-11T16:06:06.871Z",
"systemRamFreeInfo": ["15225753600"]
},
{
"reportTime": "2025-02-11T16:13:28.176Z",
"systemRamFreeInfo": ["15228182528"]
},
{
"reportTime": "2025-02-11T16:17:05.936Z",
"systemRamFreeInfo": ["15223095296"]
},
{
"reportTime": "2025-02-11T16:36:19.897Z",
"systemRamFreeInfo": ["15226126336"]
},
{
"reportTime": "2025-02-11T16:48:44.934Z",
"systemRamFreeInfo": ["15226707968"]
},
{
"reportTime": "2025-02-12T06:35:28.949Z",
"systemRamFreeInfo": ["15222706176"]
}
],
"diskVolumeReports": [
{
"volumeInfo": [
{
"volumeId": "/media/archive",
"storageTotal": "8044687360",
"storageFree": "8044687360"
},
{
"volumeId": "/media/removable",
"storageTotal": "8044687360",
"storageFree": "8044687360"
}
]
}
],
"lastKnownNetwork": [
{
"ipAddress": "192.168.0.106",
"wanIpAddress": "87.121.13.137"
}
],
"cpuInfo": [
{
"model": "AMD Ryzen 5 4500U with Radeon Graphics",
"architecture": "x64",
"maxClockSpeedKhz": 2375000,
"logicalCpus": [
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397253,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "59.509354s"
},
{
"displayName": "C1",
"sessionDuration": "1.338153s"
},
{
"displayName": "C2",
"sessionDuration": "0.241264s"
},
{
"displayName": "POLL",
"sessionDuration": "0.004477s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397372,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "58.861175s"
},
{
"displayName": "C1",
"sessionDuration": "1.335068s"
},
{
"displayName": "C2",
"sessionDuration": "0.761853s"
},
{
"displayName": "POLL",
"sessionDuration": "0.007583s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397454,
"idleDuration": "58s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "57.457528s"
},
{
"displayName": "C1",
"sessionDuration": "1.280076s"
},
{
"displayName": "C2",
"sessionDuration": "0.167642s"
},
{
"displayName": "POLL",
"sessionDuration": "0.003444s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397348,
"idleDuration": "59s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "58.906343s"
},
{
"displayName": "C1",
"sessionDuration": "1.101873s"
},
{
"displayName": "C2",
"sessionDuration": "0.119013s"
},
{
"displayName": "POLL",
"sessionDuration": "0.009095s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1383188,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "59.476621s"
},
{
"displayName": "C1",
"sessionDuration": "1.048691s"
},
{
"displayName": "C2",
"sessionDuration": "0.192808s"
},
{
"displayName": "POLL",
"sessionDuration": "0.003546s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397437,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "60.155800s"
},
{
"displayName": "C1",
"sessionDuration": "0.681644s"
},
{
"displayName": "C2",
"sessionDuration": "0.143131s"
},
{
"displayName": "POLL",
"sessionDuration": "0.004276s"
}
]
}
]
}
],
"extendedSupportEligible": false,
"chromeOsType": "chromeOsFlex",
"diskSpaceUsage": {
"capacityBytes": "549755813888",
"usedBytes": "85613068288"
}
}
}
]
}
}

View File

@@ -0,0 +1,34 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Create Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('create.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.post('/directory/v1/groups', {
email: 'NewOnes22@example.com',
name: 'Test',
description: 'test',
})
.reply(200, {
kind: 'admin#directory#group',
id: '03mzq4wv15cepg2',
etag: '"example"',
email: 'NewOnes22@example.com',
name: 'Test',
description: 'test',
adminCreated: true,
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,61 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"name": "Test",
"email": "NewOnes22@example.com",
"additionalFields": {
"description": "test"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [60, 1140],
"id": "54a9e564-bff5-4d86-b684-e5cf5b34b48c",
"name": "Create Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Create Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Create Group": [
{
"json": {
"kind": "admin#directory#group",
"id": "03mzq4wv15cepg2",
"etag": "\"example\"",
"email": "NewOnes22@example.com",
"name": "Test",
"description": "test",
"adminCreated": true
}
}
]
}
}

View File

@@ -0,0 +1,22 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Delete Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('delete.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.delete('/directory/v1/groups/01302m922pmp3e4')
.reply(204, '');
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,55 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "delete",
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [60, 1140],
"id": "9d6f8739-8a1b-4b85-9e5c-84a184e6dbaf",
"name": "Delete Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Delete Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Delete Group": [
{
"json": { "success": true }
}
]
}
}

View File

@@ -0,0 +1,38 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('get.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/groups/01302m922pmp3e4')
.reply(200, {
kind: 'admin#directory#group',
id: '01302m922pmp3e4',
etag: '"example"',
email: 'new3@example.com',
name: 'new2',
directMembersCount: '2',
description: 'new1',
adminCreated: true,
aliases: ['new2@example.com', 'new@example.com', 'NewOnes@example.com'],
nonEditableAliases: [
'NewOnes@example.com.test-google-a.com',
'new@example.com.test-google-a.com',
'new2@example.com.test-google-a.com',
'new3@example.com.test-google-a.com',
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,71 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "get",
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [80, 1120],
"id": "8d47d64b-80df-479a-8e1d-d63991f5d23b",
"name": "Get Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Group": [
{
"json": {
"kind": "admin#directory#group",
"id": "01302m922pmp3e4",
"etag": "\"example\"",
"email": "new3@example.com",
"name": "new2",
"directMembersCount": "2",
"description": "new1",
"adminCreated": true,
"aliases": ["new2@example.com", "new@example.com", "NewOnes@example.com"],
"nonEditableAliases": [
"NewOnes@example.com.test-google-a.com",
"new@example.com.test-google-a.com",
"new2@example.com.test-google-a.com",
"new3@example.com.test-google-a.com"
]
}
}
]
}
}

View File

@@ -0,0 +1,53 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get All Groups', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('getAll.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/groups')
.query({
customer: 'my_customer',
maxResults: '100',
})
.reply(200, {
kind: 'admin#directory#groups',
etag: '"test_etag"',
groups: [
{
kind: 'admin#directory#group',
id: '01x0gk373c9z46j',
etag: '"example"',
email: 'newoness@example.com',
name: 'NewOness',
directMembersCount: '1',
description: 'test',
adminCreated: true,
nonEditableAliases: ['NewOness@example.com.test-google-a.com'],
},
{
kind: 'admin#directory#group',
id: '01tuee742txc3k4',
etag: '"example"',
email: 'newonesss@example.com',
name: 'NewOne3',
directMembersCount: '0',
description: 'test',
adminCreated: true,
nonEditableAliases: ['NewOnesss@example.com.test-google-a.com'],
},
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,74 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "getAll",
"returnAll": true,
"filter": {}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [100, 1120],
"id": "30263040-3578-4ce6-b19c-f665b85ca301",
"name": "Get Many",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Many",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Many": [
{
"json": {
"kind": "admin#directory#group",
"id": "01x0gk373c9z46j",
"etag": "\"example\"",
"email": "newoness@example.com",
"name": "NewOness",
"directMembersCount": "1",
"description": "test",
"adminCreated": true,
"nonEditableAliases": ["NewOness@example.com.test-google-a.com"]
}
},
{
"json": {
"kind": "admin#directory#group",
"id": "01tuee742txc3k4",
"etag": "\"example\"",
"email": "newonesss@example.com",
"name": "NewOne3",
"directMembersCount": "0",
"description": "test",
"adminCreated": true,
"nonEditableAliases": ["NewOnesss@example.com.test-google-a.com"]
}
}
]
}
}

View File

@@ -0,0 +1,35 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Update Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('update.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.put('/directory/v1/groups/01302m922p525286')
.reply(200, {
kind: 'admin#directory#group',
id: '01302m922p525286',
etag: '"example"',
email: 'new3@example.com',
name: 'new2',
description: 'new1',
adminCreated: true,
aliases: ['new@example.com', 'NewOnes@example.com', 'new2@example.com'],
nonEditableAliases: [
'NewOnes@example.com.test-google-a.com',
'new@example.com.test-google-a.com',
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,73 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "update",
"groupId": {
"__rl": true,
"value": "01302m922p525286",
"mode": "list",
"cachedResultName": "new"
},
"updateFields": {
"description": "new1",
"email": "new3@example.com",
"name": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [80, 1100],
"id": "013eec82-8d52-4485-88eb-d9caf112d539",
"name": "Update Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Update Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Update Group": [
{
"json": {
"kind": "admin#directory#group",
"id": "01302m922p525286",
"etag": "\"example\"",
"email": "new3@example.com",
"name": "new2",
"description": "new1",
"adminCreated": true,
"aliases": ["new@example.com", "NewOnes@example.com", "new2@example.com"],
"nonEditableAliases": [
"NewOnes@example.com.test-google-a.com",
"new@example.com.test-google-a.com"
]
}
}
]
}
}

View File

@@ -0,0 +1,32 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Add User to Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('addToGroup.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/users/114393134535981252528')
.reply(200, { primaryEmail: 'newone@example.com' });
nock('https://www.googleapis.com/admin')
.post('/directory/v1/groups/01302m922pmp3e4/members', {
email: 'newone@example.com',
role: 'MEMBER',
})
.reply(200, {
kind: 'admin#directory#member',
status: 'ACTIVE',
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,60 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "addToGroup",
"userId": {
"__rl": true,
"value": "114393134535981252528",
"mode": "list",
"cachedResultName": "NewOne User"
},
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [80, 1100],
"id": "b0c8042f-4ce1-41f1-9d14-876d5cac3ccf",
"name": "Add To Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Add To Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Add To Group": [
{
"json": { "added": true }
}
]
}
}

View File

@@ -0,0 +1,51 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Create User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('create.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.post('/directory/v1/users')
.reply(200, {
kind: 'admin#directory#user',
id: '112507770188715525288',
etag: '"example"',
primaryEmail: 'new@example.com',
name: {
givenName: 'NewOne',
familyName: 'User',
},
emails: [
{
address: 'test@mail.com',
type: 'work',
},
],
phones: [
{
primary: false,
type: 'work',
value: '+1-202-555-0123',
},
],
isAdmin: false,
isDelegatedAdmin: false,
creationTime: '2024-12-20T20:48:53.000Z',
customerId: 'C4444hnz2',
orgUnitPath: '/',
isMailboxSetup: false,
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,107 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"firstName": "NewOne",
"lastName": "User",
"password": "12345678",
"username": "new",
"domain": "example.com",
"additionalFields": {
"changePasswordAtNextLogin": true,
"phoneUi": {
"phoneValues": [
{
"value": "+1-202-555-0123"
}
]
},
"emailUi": {
"emailValues": [
{
"address": "test@mail.com"
}
]
},
"roles": ["groupsAdmin"],
"customFields": {
"fieldValues": [
{
"schemaName": "NewTest",
"fieldName": "test",
"value": "test"
}
]
}
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [100, 1100],
"id": "54227da8-70ad-456a-8d75-f7e28d514e90",
"name": "Create User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Create User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Create User": [
{
"json": {
"kind": "admin#directory#user",
"id": "112507770188715525288",
"etag": "\"example\"",
"primaryEmail": "new@example.com",
"name": {
"givenName": "NewOne",
"familyName": "User"
},
"isAdmin": false,
"isDelegatedAdmin": false,
"creationTime": "2024-12-20T20:48:53.000Z",
"emails": [
{
"address": "test@mail.com",
"type": "work"
}
],
"phones": [
{
"value": "+1-202-555-0123",
"primary": false,
"type": "work"
}
],
"customerId": "C4444hnz2",
"orgUnitPath": "/",
"isMailboxSetup": false
}
}
]
}
}

View File

@@ -0,0 +1,23 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Delete User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('delete.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.delete('/directory/v1/users/114393134535981252212')
.reply(200, {});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,56 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "delete",
"userId": {
"__rl": true,
"value": "114393134535981252212",
"mode": "list",
"cachedResultName": "NewOne22 User22"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [120, 1100],
"id": "39f9a9c0-2ea5-45b2-a346-55017dfa4e43",
"name": "Delete User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Delete User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Delete User": [
{
"json": {
"deleted": true
}
}
]
}
}

View File

@@ -0,0 +1,43 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('get.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/users/112507770188715252026')
.query({ projection: 'basic' })
.reply(200, {
kind: 'admin#directory#user',
id: '112507770188715252026',
primaryEmail: 'new@example.com',
name: {
givenName: 'New One',
familyName: 'User',
fullName: 'New One User',
},
isAdmin: false,
lastLoginTime: '1970-01-01T00:00:00.000Z',
creationTime: '2024-12-20T20:48:53.000Z',
suspended: true,
emails: [
{
address: 'new@example.com',
primary: true,
},
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,67 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "get",
"userId": {
"__rl": true,
"value": "112507770188715252026",
"mode": "list",
"cachedResultName": "NewOne22 User22"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [120, 1100],
"id": "b39d3a72-6e81-4219-82eb-f39d99eace16",
"name": "Get User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get User": [
{
"json": {
"kind": "admin#directory#user",
"id": "112507770188715252026",
"primaryEmail": "new@example.com",
"name": {
"givenName": "New One",
"familyName": "User",
"fullName": "New One User"
},
"isAdmin": false,
"lastLoginTime": "1970-01-01T00:00:00.000Z",
"creationTime": "2024-12-20T20:48:53.000Z",
"suspended": true
}
}
]
}
}

View File

@@ -0,0 +1,72 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get All Users', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('getAll.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/users')
.query({
projection: 'basic',
customer: 'my_customer',
maxResults: '100',
})
.reply(200, {
kind: 'admin#directory#users',
users: [
{
kind: 'admin#directory#user',
id: '112507770188715252055',
primaryEmail: 'new@example.com',
name: {
givenName: 'New',
familyName: 'User',
fullName: 'New User',
},
isAdmin: false,
lastLoginTime: '1970-01-01T00:00:00.000Z',
creationTime: '2024-12-20T20:48:53.000Z',
suspended: true,
emails: [
{
address: 'new@example.com',
primary: true,
},
],
},
{
kind: 'admin#directory#user',
id: '222459372679230452528',
primaryEmail: 'test33@example.com',
name: {
givenName: 'New',
familyName: 'Test',
fullName: 'New Test',
},
isAdmin: true,
lastLoginTime: '2024-12-19T08:39:56.000Z',
creationTime: '2024-09-06T11:48:38.000Z',
suspended: false,
emails: [
{
address: 'test33@example.com',
primary: true,
},
],
},
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,78 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "getAll",
"filter": {}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [140, 1100],
"id": "4107ee9d-37e1-4d85-9099-25ec13211ee1",
"name": "Get Many",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Many",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Many": [
{
"json": {
"kind": "admin#directory#user",
"id": "112507770188715252055",
"primaryEmail": "new@example.com",
"name": {
"givenName": "New",
"familyName": "User",
"fullName": "New User"
},
"isAdmin": false,
"lastLoginTime": "1970-01-01T00:00:00.000Z",
"creationTime": "2024-12-20T20:48:53.000Z",
"suspended": true
}
},
{
"json": {
"kind": "admin#directory#user",
"id": "222459372679230452528",
"primaryEmail": "test33@example.com",
"name": {
"givenName": "New",
"familyName": "Test",
"fullName": "New Test"
},
"isAdmin": true,
"lastLoginTime": "2024-12-19T08:39:56.000Z",
"creationTime": "2024-09-06T11:48:38.000Z",
"suspended": false
}
}
]
}
}

View File

@@ -0,0 +1,23 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Remove User from Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('removeFromGroup.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.delete('/directory/v1/groups/01302m922pmp3e4/members/114393134535981252528')
.reply(200, {});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,60 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "removeFromGroup",
"userId": {
"__rl": true,
"value": "114393134535981252528",
"mode": "list",
"cachedResultName": "New User"
},
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [140, 1100],
"id": "9a323b95-1495-487f-ba27-2df737bd5bdf",
"name": "Remove From Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Remove From Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Remove From Group": [
{
"json": { "removed": true }
}
]
}
}

View File

@@ -0,0 +1,82 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Update User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('update.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.put('/directory/v1/users/101071249467630629404', {
name: {
givenName: 'test',
familyName: 'new',
},
primaryEmail: 'one@example.com',
phones: [
{
type: 'assistant',
value: '123',
primary: true,
},
],
emails: [
{
address: 'newone@example.com',
type: 'home',
},
],
})
.reply(200, {
kind: 'admin#directory#user',
id: '101071249467630629404',
etag: '"example"',
primaryEmail: 'one@example.com',
name: {
givenName: 'test',
familyName: 'new',
},
isAdmin: false,
isDelegatedAdmin: false,
lastLoginTime: '1970-01-01T00:00:00.000Z',
creationTime: '2025-03-26T21:28:53.000Z',
agreedToTerms: false,
suspended: false,
archived: false,
changePasswordAtNextLogin: false,
ipWhitelisted: false,
emails: [
{
address: 'newone@example.com',
type: 'home',
},
],
phones: [
{
value: '123',
primary: true,
type: 'assistant',
},
],
aliases: ['new22@example.com'],
nonEditableAliases: ['new22@example.com.test-google-a.com'],
customerId: 'C0442hnz1',
orgUnitPath: '/',
isMailboxSetup: false,
includeInGlobalAddressList: true,
thumbnailPhotoUrl: '//example',
thumbnailPhotoEtag: '"example"',
recoveryEmail: '',
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,117 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "update",
"userId": {
"__rl": true,
"value": "101071249467630629404",
"mode": "list",
"cachedResultName": "NewOne22 User22"
},
"updateFields": {
"archived": true,
"firstName": "test",
"lastName": "new",
"phoneUi": {
"phoneValues": [
{
"type": "assistant",
"value": "123",
"primary": true
}
]
},
"primaryEmail": "one@example.com",
"emailUi": {
"emailValues": [
{
"type": "home",
"address": "newone@example.com"
}
]
}
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [140, 1100],
"id": "b4cd1391-cfdc-4f36-8c2f-f18a9ad4f795",
"name": "Update User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Update User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Update User": [
{
"json": {
"kind": "admin#directory#user",
"id": "101071249467630629404",
"etag": "\"example\"",
"primaryEmail": "one@example.com",
"name": {
"givenName": "test",
"familyName": "new"
},
"isAdmin": false,
"isDelegatedAdmin": false,
"lastLoginTime": "1970-01-01T00:00:00.000Z",
"creationTime": "2025-03-26T21:28:53.000Z",
"agreedToTerms": false,
"suspended": false,
"archived": false,
"changePasswordAtNextLogin": false,
"ipWhitelisted": false,
"emails": [
{
"address": "newone@example.com",
"type": "home"
}
],
"phones": [
{
"value": "123",
"primary": true,
"type": "assistant"
}
],
"aliases": ["new22@example.com"],
"nonEditableAliases": ["new22@example.com.test-google-a.com"],
"customerId": "C0442hnz1",
"orgUnitPath": "/",
"isMailboxSetup": false,
"includeInGlobalAddressList": true,
"thumbnailPhotoUrl": "//example",
"thumbnailPhotoEtag": "\"example\"",
"recoveryEmail": ""
}
}
]
}
}