n8n/packages/nodes-base/nodes/Google/MyBusiness/GenericFunctions.ts
2024-10-16 16:18:53 +01:00

521 lines
15 KiB
TypeScript

import {
NodeApiError,
NodeOperationError,
type DeclarativeRestApiSettings,
type IDataObject,
type IExecutePaginationFunctions,
type IExecuteSingleFunctions,
type IHttpRequestMethods,
type IHttpRequestOptions,
type ILoadOptionsFunctions,
type IN8nHttpFullResponse,
type INodeExecutionData,
type INodeListSearchItems,
type INodeListSearchResult,
type IPollFunctions,
type JsonObject,
} from 'n8n-workflow';
import type { ITimeInterval } from './Interfaces';
const addOptName = 'additionalOptions';
const possibleRootProperties = ['localPosts', 'reviews'];
const getAllParams = (execFns: IExecuteSingleFunctions): Record<string, unknown> => {
const params = execFns.getNode().parameters;
const additionalOptions = execFns.getNodeParameter(addOptName, {}) as Record<string, unknown>;
// Merge standard parameters with additional options from the node parameters
return { ...params, ...additionalOptions };
};
/* Helper function to adjust date-time parameters for API requests */
export async function handleDatesPresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const params = getAllParams(this);
const body = Object.assign({}, opts.body) as IDataObject;
const event = (body.event as IDataObject) ?? ({} as IDataObject);
if (!params.startDateTime && !params.startDate && !params.endDateTime && !params.endDate) {
return opts;
}
const createDateTimeObject = (dateString: string) => {
const date = new Date(dateString);
return {
date: {
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
day: date.getUTCDate(),
},
time: dateString.includes('T')
? {
hours: date.getUTCHours(),
minutes: date.getUTCMinutes(),
seconds: date.getUTCSeconds(),
nanos: 0,
}
: undefined,
};
};
// Convert start and end date-time parameters if provided
const startDateTime =
params.startDateTime || params.startDate
? createDateTimeObject((params.startDateTime || params.startDate) as string)
: null;
const endDateTime =
params.endDateTime || params.endDate
? createDateTimeObject((params.endDateTime || params.endDate) as string)
: null;
const schedule: Partial<ITimeInterval> = {
startDate: startDateTime?.date,
endDate: endDateTime?.date,
startTime: startDateTime?.time,
endTime: endDateTime?.time,
};
event.schedule = schedule;
Object.assign(body, { event });
opts.body = body;
return opts;
}
/* Helper function adding update mask to the request */
export async function addUpdateMaskPresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const additionalOptions = this.getNodeParameter('additionalOptions') as IDataObject;
const propertyMapping: { [key: string]: string } = {
postType: 'topicType',
actionType: 'actionType',
callToActionType: 'callToAction.actionType',
url: 'callToAction.url',
startDateTime: 'event.schedule.startDate,event.schedule.startTime',
endDateTime: 'event.schedule.endDate,event.schedule.endTime',
title: 'event.title',
startDate: 'event.schedule.startDate',
endDate: 'event.schedule.endDate',
couponCode: 'offer.couponCode',
redeemOnlineUrl: 'offer.redeemOnlineUrl',
termsAndConditions: 'offer.termsAndConditions',
};
if (Object.keys(additionalOptions).length) {
const updateMask = Object.keys(additionalOptions)
.map((key) => propertyMapping[key] || key)
.join(',');
opts.qs = {
...opts.qs,
updateMask,
};
}
return opts;
}
/* Helper function to handle pagination */
export async function handlePagination(
this: IExecutePaginationFunctions,
resultOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]> {
const aggregatedResult: IDataObject[] = [];
let nextPageToken: string | undefined;
const returnAll = this.getNodeParameter('returnAll') as boolean;
let limit = 100;
if (!returnAll) {
limit = this.getNodeParameter('limit') as number;
resultOptions.maxResults = limit;
}
resultOptions.paginate = true;
do {
if (nextPageToken) {
resultOptions.options.qs = { ...resultOptions.options.qs, pageToken: nextPageToken };
}
const responseData = await this.makeRoutingRequest(resultOptions);
for (const page of responseData) {
for (const prop of possibleRootProperties) {
if (page.json[prop]) {
const currentData = page.json[prop] as IDataObject[];
aggregatedResult.push(...currentData);
}
}
if (!returnAll && aggregatedResult.length >= limit) {
return aggregatedResult.slice(0, limit).map((item) => ({ json: item }));
}
nextPageToken = page.json.nextPageToken as string | undefined;
}
} while (nextPageToken);
return aggregatedResult.map((item) => ({ json: item }));
}
/* Helper functions to handle errors */
export async function handleErrorsDeletePost(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (response.statusCode < 200 || response.statusCode >= 300) {
const post = this.getNodeParameter('post', undefined) as IDataObject;
// Provide a better error message
if (post && response.statusCode === 404) {
throw new NodeOperationError(
this.getNode(),
'The post you are deleting could not be found. Adjust the "post" parameter setting to delete the post correctly.',
);
}
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
message: response.statusMessage,
httpCode: response.statusCode.toString(),
});
}
return data;
}
export async function handleErrorsGetPost(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (response.statusCode < 200 || response.statusCode >= 300) {
const post = this.getNodeParameter('post', undefined) as IDataObject;
// Provide a better error message
if (post && response.statusCode === 404) {
throw new NodeOperationError(
this.getNode(),
'The post you are requesting could not be found. Adjust the "post" parameter setting to retrieve the post correctly.',
);
}
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
message: response.statusMessage,
httpCode: response.statusCode.toString(),
});
}
return data;
}
export async function handleErrorsUpdatePost(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (response.statusCode < 200 || response.statusCode >= 300) {
const post = this.getNodeParameter('post') as IDataObject;
const additionalOptions = this.getNodeParameter('additionalOptions') as IDataObject;
// Provide a better error message
if (post && response.statusCode === 404) {
throw new NodeOperationError(
this.getNode(),
'The post you are updating could not be found. Adjust the "post" parameter setting to update the post correctly.',
);
}
// Do not throw an error if the user didn't set additional options (a hint will be shown)
if (response.statusCode === 400 && Object.keys(additionalOptions).length === 0) {
return [{ json: { success: true } }];
}
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
message: response.statusMessage,
httpCode: response.statusCode.toString(),
});
}
return data;
}
export async function handleErrorsDeleteReply(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (response.statusCode < 200 || response.statusCode >= 300) {
const review = this.getNodeParameter('review', undefined) as IDataObject;
// Provide a better error message
if (review && response.statusCode === 404) {
throw new NodeOperationError(
this.getNode(),
'The review you are deleting could not be found. Adjust the "review" parameter setting to update the review correctly.',
);
}
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
message: response.statusMessage,
httpCode: response.statusCode.toString(),
});
}
return data;
}
export async function handleErrorsGetReview(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (response.statusCode < 200 || response.statusCode >= 300) {
const review = this.getNodeParameter('review', undefined) as IDataObject;
// Provide a better error message
if (review && response.statusCode === 404) {
throw new NodeOperationError(
this.getNode(),
'The review you are requesting could not be found. Adjust the "review" parameter setting to update the review correctly.',
);
}
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
message: response.statusMessage,
httpCode: response.statusCode.toString(),
});
}
return data;
}
export async function handleErrorsReplyToReview(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (response.statusCode < 200 || response.statusCode >= 300) {
const review = this.getNodeParameter('review', undefined) as IDataObject;
// Provide a better error message
if (review && response.statusCode === 404) {
throw new NodeOperationError(
this.getNode(),
'The review you are replying to could not be found. Adjust the "review" parameter setting to reply to the review correctly.',
);
}
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
message: response.statusMessage,
httpCode: response.statusCode.toString(),
});
}
return data;
}
/* Helper function used in listSearch methods */
export async function googleApiRequest(
this: ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
qs: IDataObject = {},
url?: string,
): Promise<IDataObject> {
const options: IHttpRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
url: url ?? `https://mybusiness.googleapis.com/v4${resource}`,
json: true,
};
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
return (await this.helpers.httpRequestWithAuthentication.call(
this,
'googleMyBusinessOAuth2Api',
options,
)) as IDataObject;
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
/* listSearch methods */
export async function searchAccounts(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
// Docs for this API call can be found here:
// https://developers.google.com/my-business/reference/accountmanagement/rest/v1/accounts/list
const query: IDataObject = {};
if (paginationToken) {
query.pageToken = paginationToken;
}
const responseData: IDataObject = await googleApiRequest.call(
this,
'GET',
'',
{},
{
pageSize: 20,
...query,
},
'https://mybusinessaccountmanagement.googleapis.com/v1/accounts',
);
const accounts = responseData.accounts as Array<{ name: string; accountName: string }>;
const results: INodeListSearchItems[] = accounts
.map((a) => ({
name: a.accountName,
value: a.name,
}))
.filter(
(a) =>
!filter ||
a.name.toLowerCase().includes(filter.toLowerCase()) ||
a.value.toLowerCase().includes(filter.toLowerCase()),
)
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
});
return { results, paginationToken: responseData.nextPageToken };
}
export async function searchLocations(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
// Docs for this API call can be found here:
// https://developers.google.com/my-business/reference/businessinformation/rest/v1/accounts.locations/list
const query: IDataObject = {};
if (paginationToken) {
query.pageToken = paginationToken;
}
const account = (this.getNodeParameter('account') as IDataObject).value as string;
const responseData: IDataObject = await googleApiRequest.call(
this,
'GET',
'',
{},
{
readMask: 'name',
pageSize: 100,
...query,
},
`https://mybusinessbusinessinformation.googleapis.com/v1/${account}/locations`,
);
const locations = responseData.locations as Array<{ name: string }>;
const results: INodeListSearchItems[] = locations
.map((a) => ({
name: a.name,
value: a.name,
}))
.filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
});
return { results, paginationToken: responseData.nextPageToken };
}
export async function searchReviews(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const query: IDataObject = {};
if (paginationToken) {
query.pageToken = paginationToken;
}
const account = (this.getNodeParameter('account') as IDataObject).value as string;
const location = (this.getNodeParameter('location') as IDataObject).value as string;
const responseData: IDataObject = await googleApiRequest.call(
this,
'GET',
`/${account}/${location}/reviews`,
{},
{
pageSize: 50,
...query,
},
);
const reviews = responseData.reviews as Array<{ name: string; comment: string }>;
const results: INodeListSearchItems[] = reviews
.map((a) => ({
name: a.comment,
value: a.name,
}))
.filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
});
return { results, paginationToken: responseData.nextPageToken };
}
export async function searchPosts(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const query: IDataObject = {};
if (paginationToken) {
query.pageToken = paginationToken;
}
const account = (this.getNodeParameter('account') as IDataObject).value as string;
const location = (this.getNodeParameter('location') as IDataObject).value as string;
const responseData: IDataObject = await googleApiRequest.call(
this,
'GET',
`/${account}/${location}/localPosts`,
{},
{
pageSize: 100,
...query,
},
);
const localPosts = responseData.localPosts as Array<{ name: string; summary: string }>;
const results: INodeListSearchItems[] = localPosts
.map((a) => ({
name: a.summary,
value: a.name,
}))
.filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
});
return { results, paginationToken: responseData.nextPageToken };
}