n8n-nodes-google-search-console-complete
Version:
Complete n8n node for Google Search Console API - 7 operations with dual auth support (OAuth2 & Service Account)
514 lines (513 loc) • 26.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoogleSearchConsole = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const googleapis_1 = require("googleapis");
function toGscDate(d) {
if (!d)
return '';
const dt = new Date(d);
if (Number.isNaN(dt.getTime()))
return '';
return dt.toISOString().split('T')[0];
}
function defaultedRange(start, end) {
const endDt = end ? new Date(end) : new Date();
const startDt = start ? new Date(start) : new Date(endDt.getTime() - 28 * 86400000);
return { startDate: toGscDate(start !== null && start !== void 0 ? start : startDt.toISOString()), endDate: toGscDate(end !== null && end !== void 0 ? end : endDt.toISOString()) };
}
function rangeFromPreset(mode, customStart, customEnd) {
const end = customEnd ? new Date(customEnd) : new Date();
let start = customStart ? new Date(customStart) : new Date(end);
if (mode !== 'custom') {
const e = new Date(end);
const s = new Date(end);
switch (mode) {
case 'last7d':
s.setDate(e.getDate() - 7);
break;
case 'last28d':
s.setDate(e.getDate() - 28);
break;
case 'last3mo':
s.setMonth(e.getMonth() - 3);
break;
case 'last12mo':
s.setMonth(e.getMonth() - 12);
break;
default: s.setDate(e.getDate() - 28);
}
return { startDate: toGscDate(s.toISOString()), endDate: toGscDate(e.toISOString()) };
}
return defaultedRange(customStart, customEnd);
}
function mapRow(dimensions, row) {
const obj = {};
if (Array.isArray(row.keys)) {
dimensions.forEach((d, idx) => { obj[d] = row.keys[idx]; });
}
obj.clicks = row.clicks;
obj.impressions = row.impressions;
obj.ctr = row.ctr;
obj.position = row.position;
return obj;
}
// Helper function for dual authentication support
async function makeAuthenticatedRequest(ctx, options) {
// Try OAuth2 first, then Service Account
try {
const oauthCredentials = await ctx.getCredentials('GoogleSearchConsoleOAuth2Api');
if (oauthCredentials) {
return await ctx.helpers.httpRequestWithAuthentication.call(ctx, 'GoogleSearchConsoleOAuth2Api', options);
}
}
catch (error) {
// OAuth2 not available, try Service Account
}
try {
const serviceCredentials = await ctx.getCredentials('GoogleSearchConsoleServiceAccount');
if (serviceCredentials) {
// Initialize Google Auth with Service Account
const auth = new googleapis_1.google.auth.GoogleAuth({
credentials: {
client_email: serviceCredentials.email,
private_key: serviceCredentials.privateKey.replace(/\\n/g, '\n'),
},
scopes: [
'https://www.googleapis.com/auth/webmasters.readonly',
'https://www.googleapis.com/auth/webmasters',
],
});
const client = await auth.getClient();
const accessToken = await auth.getAccessToken();
// Add authorization header
const requestOptions = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`,
},
};
return await ctx.helpers.httpRequest(requestOptions);
}
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Authentication failed: ${error.message}`);
}
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'No valid credentials provided. Please configure either OAuth2 or Service Account credentials.');
}
async function fetchAllRows(ctx, siteUrl, body, targetLimit) {
var _a;
const rows = [];
let startRow = 0;
const perRequest = Math.max(100, Math.min((_a = body.rowLimit) !== null && _a !== void 0 ? _a : 1000, 25000));
while (rows.length < targetLimit) {
const resp = await makeAuthenticatedRequest(ctx, {
method: 'POST',
url: `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`,
body: { ...body, rowLimit: perRequest, startRow },
});
const chunk = Array.isArray(resp === null || resp === void 0 ? void 0 : resp.rows) ? resp.rows : [];
if (chunk.length === 0)
break;
rows.push(...chunk);
startRow += chunk.length;
if (chunk.length < perRequest)
break;
if (rows.length >= targetLimit)
break;
}
return rows.slice(0, targetLimit);
}
/* ========= Node ========= */
class GoogleSearchConsole {
constructor() {
this.description = {
displayName: 'Google Search Console',
name: 'googleSearchConsole',
icon: 'file:googlesearchconsole.svg',
group: ['transform'],
version: 1,
description: 'Connect to Google Search Console API',
defaults: { name: 'Search Console' },
subtitle: '={{$parameter.operation}}',
inputs: ['main'],
outputs: ['main'],
usableAsTool: true,
credentials: [
{ name: 'GoogleSearchConsoleOAuth2Api', required: false },
{ name: 'GoogleSearchConsoleServiceAccount', required: false },
],
requestDefaults: {
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [{ name: 'Site', value: 'site' }],
default: 'site',
required: true,
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{ name: 'Get Sites', value: 'getSites', action: 'List verified sites' },
{ name: 'Get Page Insights', value: 'getPageInsights', action: 'Query search analytics' },
{ name: 'Inspect URL', value: 'inspectUrl', action: 'URL Inspection (index status)' },
{ name: 'List Sitemaps', value: 'listSitemaps', action: 'List sitemaps for a site' },
{ name: 'Get Sitemap Details', value: 'getSitemap', action: 'Get sitemap details' },
{ name: 'Submit Sitemap', value: 'submitSitemap', action: 'Submit a sitemap' },
{ name: 'Delete Sitemap', value: 'deleteSitemap', action: 'Delete a sitemap' },
],
default: 'getSites',
required: true,
displayOptions: { show: { resource: ['site'] } },
},
/* ---------- getPageInsights ---------- */
{
displayName: 'Site URL Mode',
name: 'siteUrlMode',
type: 'options',
options: [
{ name: 'Pick from My Verified Sites', value: 'list' },
{ name: 'Enter Manually', value: 'manual' },
],
default: 'list',
displayOptions: { show: { resource: ['site'], operation: ['getPageInsights'] } },
},
{
displayName: 'Site URL',
name: 'siteUrl',
type: 'options',
typeOptions: { loadOptionsMethod: 'getVerifiedSites' },
displayOptions: {
show: { resource: ['site'], operation: ['getPageInsights'], siteUrlMode: ['list'] },
},
default: '',
required: true,
},
{
displayName: 'Site URL (Manual)',
name: 'siteUrlManual',
type: 'string',
placeholder: 'https://example.com/ or sc-domain:example.com',
hint: 'Enter your verified property URL.',
displayOptions: {
show: { resource: ['site'], operation: ['getPageInsights'], siteUrlMode: ['manual'] },
},
default: '',
required: true,
},
{
displayName: 'Date Range',
name: 'dateRangeMode',
type: 'options',
options: [
{ name: 'Last 7 Days', value: 'last7d' },
{ name: 'Last 28 Days', value: 'last28d' },
{ name: 'Last 3 Months', value: 'last3mo' },
{ name: 'Last 12 Months', value: 'last12mo' },
{ name: 'Custom', value: 'custom' },
],
default: 'last28d',
hint: 'Select preset or Custom to set exact dates.',
displayOptions: { show: { resource: ['site'], operation: ['getPageInsights'] } },
},
{
displayName: 'Start Date',
name: 'startDate',
type: 'dateTime',
hint: 'Shown only if Date Range is Custom.',
displayOptions: { show: { resource: ['site'], operation: ['getPageInsights'], dateRangeMode: ['custom'] } },
default: '',
},
{
displayName: 'End Date',
name: 'endDate',
type: 'dateTime',
hint: 'Shown only if Date Range is Custom.',
displayOptions: { show: { resource: ['site'], operation: ['getPageInsights'], dateRangeMode: ['custom'] } },
default: '',
},
{
displayName: 'Row Limit',
name: 'rowLimit',
type: 'number',
typeOptions: { minValue: 1, maxValue: 25000 },
default: 1000,
displayOptions: { show: { resource: ['site'], operation: ['getPageInsights'] } },
},
{
displayName: 'Search Type',
name: 'searchType',
type: 'options',
options: [
{ name: 'Web', value: 'web' },
{ name: 'Image', value: 'image' },
{ name: 'Video', value: 'video' },
{ name: 'News', value: 'news' },
],
default: 'web',
displayOptions: { show: { resource: ['site'], operation: ['getPageInsights'] } },
},
{
displayName: 'Dimensions',
name: 'dimensions',
type: 'multiOptions',
options: [
{ name: 'Date', value: 'date' },
{ name: 'Page', value: 'page' },
{ name: 'Query', value: 'query' },
{ name: 'Country', value: 'country' },
{ name: 'Device', value: 'device' },
],
default: ['page'],
displayOptions: { show: { resource: ['site'], operation: ['getPageInsights'] } },
},
/* ---------- inspectUrl ---------- */
{
displayName: 'Site URL Mode',
name: 'inspectSiteUrlMode',
type: 'options',
options: [
{ name: 'Pick from My Verified Sites', value: 'list' },
{ name: 'Enter Manually', value: 'manual' },
],
default: 'list',
displayOptions: { show: { resource: ['site'], operation: ['inspectUrl'] } },
},
{
displayName: 'Site URL',
name: 'inspectSiteUrl',
type: 'options',
typeOptions: { loadOptionsMethod: 'getVerifiedSites' },
displayOptions: {
show: { resource: ['site'], operation: ['inspectUrl'], inspectSiteUrlMode: ['list'] },
},
default: '',
required: true,
},
{
displayName: 'Site URL (Manual)',
name: 'inspectSiteUrlManual',
type: 'string',
placeholder: 'https://example.com/ or sc-domain:example.com',
displayOptions: {
show: { resource: ['site'], operation: ['inspectUrl'], inspectSiteUrlMode: ['manual'] },
},
default: '',
required: true,
},
{
displayName: 'Inspection URL',
name: 'inspectionUrl',
type: 'string',
displayOptions: { show: { resource: ['site'], operation: ['inspectUrl'] } },
default: '',
required: true,
},
{
displayName: 'Language Code',
name: 'languageCode',
type: 'string',
displayOptions: { show: { resource: ['site'], operation: ['inspectUrl'] } },
default: '',
},
/* ---------- Sitemap Operations ---------- */
{
displayName: 'Site URL Mode',
name: 'sitemapSiteUrlMode',
type: 'options',
options: [
{ name: 'Pick from My Verified Sites', value: 'list' },
{ name: 'Enter Manually', value: 'manual' },
],
default: 'list',
displayOptions: { show: { resource: ['site'], operation: ['listSitemaps', 'getSitemap', 'submitSitemap', 'deleteSitemap'] } },
},
{
displayName: 'Site URL',
name: 'sitemapSiteUrl',
type: 'options',
typeOptions: { loadOptionsMethod: 'getVerifiedSites' },
displayOptions: {
show: { resource: ['site'], operation: ['listSitemaps', 'getSitemap', 'submitSitemap', 'deleteSitemap'], sitemapSiteUrlMode: ['list'] },
},
default: '',
required: true,
},
{
displayName: 'Site URL (Manual)',
name: 'sitemapSiteUrlManual',
type: 'string',
placeholder: 'https://example.com/ or sc-domain:example.com',
displayOptions: {
show: { resource: ['site'], operation: ['listSitemaps', 'getSitemap', 'submitSitemap', 'deleteSitemap'], sitemapSiteUrlMode: ['manual'] },
},
default: '',
required: true,
},
{
displayName: 'Sitemap URL',
name: 'sitemapUrl',
type: 'string',
placeholder: 'https://example.com/sitemap.xml',
displayOptions: { show: { resource: ['site'], operation: ['getSitemap', 'submitSitemap', 'deleteSitemap'] } },
default: '',
required: true,
description: 'The URL of the sitemap file',
},
],
};
this.methods = {
loadOptions: {
async getVerifiedSites() {
try {
const resp = await makeAuthenticatedRequest(this, { method: 'GET', url: 'https://www.googleapis.com/webmasters/v3/sites' });
const sites = Array.isArray(resp === null || resp === void 0 ? void 0 : resp.siteEntry) ? resp.siteEntry : [];
if (!sites.length) {
return [{ name: 'No verified properties found', value: '__NO_SITES__' }];
}
const sorted = [...sites].sort((a, b) => {
const av = a.siteUrl.startsWith('sc-domain:') ? 0 : 1;
const bv = b.siteUrl.startsWith('sc-domain:') ? 0 : 1;
return av - bv || a.siteUrl.localeCompare(b.siteUrl);
});
return sorted.map((s) => ({
name: `${s.siteUrl}${s.permissionLevel ? ` (${s.permissionLevel})` : ''}`,
value: s.siteUrl,
description: s.permissionLevel || '',
}));
}
catch (err) {
return [{ name: `Error: ${(err === null || err === void 0 ? void 0 : err.message) || 'Failed to load sites'}`, value: '__NO_SITES__' }];
}
},
},
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
for (let i = 0; i < items.length; i++) {
const operation = this.getNodeParameter('operation', i);
const pushOk = (json) => returnData.push({ json, pairedItem: { item: i } });
const pushErr = (e) => { var _a; if (this.continueOnFail())
pushOk({ error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e });
else
throw e; };
try {
if (operation === 'getSites') {
const resp = await makeAuthenticatedRequest(this, { method: 'GET', url: 'https://www.googleapis.com/webmasters/v3/sites' });
const sites = Array.isArray(resp === null || resp === void 0 ? void 0 : resp.siteEntry) ? resp.siteEntry : [];
sites.forEach((s) => pushOk({ resource: 'site', operation: 'getSites', ...s }));
}
if (operation === 'getPageInsights') {
const siteUrlMode = this.getNodeParameter('siteUrlMode', i);
const siteUrl = siteUrlMode === 'manual'
? this.getNodeParameter('siteUrlManual', i).trim()
: this.getNodeParameter('siteUrl', i).trim();
if (!siteUrl || siteUrl === '__NO_SITES__')
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No site selected.', { itemIndex: i });
const dateRangeMode = this.getNodeParameter('dateRangeMode', i);
const startDateParam = this.getNodeParameter('startDate', i, '');
const endDateParam = this.getNodeParameter('endDate', i, '');
const { startDate, endDate } = rangeFromPreset(dateRangeMode, startDateParam, endDateParam);
const rowLimit = this.getNodeParameter('rowLimit', i);
const searchType = this.getNodeParameter('searchType', i);
const dimensions = this.getNodeParameter('dimensions', i);
const body = { startDate, endDate, dimensions, rowLimit: Math.max(1, Math.min(rowLimit, 25000)), searchType };
const rows = await fetchAllRows(this, siteUrl, body, rowLimit);
rows.forEach((r) => pushOk({ resource: 'site', operation: 'getPageInsights', ...mapRow(dimensions, r) }));
}
if (operation === 'inspectUrl') {
const siteUrlMode = this.getNodeParameter('inspectSiteUrlMode', i);
const siteUrl = siteUrlMode === 'manual'
? this.getNodeParameter('inspectSiteUrlManual', i).trim()
: this.getNodeParameter('inspectSiteUrl', i).trim();
const inspectionUrl = this.getNodeParameter('inspectionUrl', i).trim();
const languageCode = this.getNodeParameter('languageCode', i) || '';
const resp = await makeAuthenticatedRequest(this, {
method: 'POST',
url: 'https://searchconsole.googleapis.com/v1/urlInspection/index:inspect',
body: { inspectionUrl, siteUrl, languageCode: languageCode || undefined },
});
const result = (resp === null || resp === void 0 ? void 0 : resp.inspectionResult) || {};
pushOk({ resource: 'site', operation: 'inspectUrl', ...result });
}
/* ---------- Sitemap Operations ---------- */
if (operation === 'listSitemaps') {
const siteUrlMode = this.getNodeParameter('sitemapSiteUrlMode', i);
const siteUrl = siteUrlMode === 'manual'
? this.getNodeParameter('sitemapSiteUrlManual', i).trim()
: this.getNodeParameter('sitemapSiteUrl', i).trim();
if (!siteUrl || siteUrl === '__NO_SITES__')
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No site selected.', { itemIndex: i });
const resp = await makeAuthenticatedRequest(this, {
method: 'GET',
url: `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps`,
});
const sitemaps = Array.isArray(resp === null || resp === void 0 ? void 0 : resp.sitemap) ? resp.sitemap : [];
sitemaps.forEach((sitemap) => pushOk({ resource: 'site', operation: 'listSitemaps', siteUrl, ...sitemap }));
}
if (operation === 'getSitemap') {
const siteUrlMode = this.getNodeParameter('sitemapSiteUrlMode', i);
const siteUrl = siteUrlMode === 'manual'
? this.getNodeParameter('sitemapSiteUrlManual', i).trim()
: this.getNodeParameter('sitemapSiteUrl', i).trim();
const sitemapUrl = this.getNodeParameter('sitemapUrl', i).trim();
if (!siteUrl || siteUrl === '__NO_SITES__')
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No site selected.', { itemIndex: i });
if (!sitemapUrl)
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Sitemap URL is required.', { itemIndex: i });
const resp = await makeAuthenticatedRequest(this, {
method: 'GET',
url: `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(sitemapUrl)}`,
});
pushOk({ resource: 'site', operation: 'getSitemap', siteUrl, sitemapUrl, ...resp });
}
if (operation === 'submitSitemap') {
const siteUrlMode = this.getNodeParameter('sitemapSiteUrlMode', i);
const siteUrl = siteUrlMode === 'manual'
? this.getNodeParameter('sitemapSiteUrlManual', i).trim()
: this.getNodeParameter('sitemapSiteUrl', i).trim();
const sitemapUrl = this.getNodeParameter('sitemapUrl', i).trim();
if (!siteUrl || siteUrl === '__NO_SITES__')
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No site selected.', { itemIndex: i });
if (!sitemapUrl)
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Sitemap URL is required.', { itemIndex: i });
await makeAuthenticatedRequest(this, {
method: 'PUT',
url: `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(sitemapUrl)}`,
});
pushOk({ resource: 'site', operation: 'submitSitemap', siteUrl, sitemapUrl, success: true, message: 'Sitemap submitted successfully' });
}
if (operation === 'deleteSitemap') {
const siteUrlMode = this.getNodeParameter('sitemapSiteUrlMode', i);
const siteUrl = siteUrlMode === 'manual'
? this.getNodeParameter('sitemapSiteUrlManual', i).trim()
: this.getNodeParameter('sitemapSiteUrl', i).trim();
const sitemapUrl = this.getNodeParameter('sitemapUrl', i).trim();
if (!siteUrl || siteUrl === '__NO_SITES__')
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No site selected.', { itemIndex: i });
if (!sitemapUrl)
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Sitemap URL is required.', { itemIndex: i });
await makeAuthenticatedRequest(this, {
method: 'DELETE',
url: `https://www.googleapis.com/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(sitemapUrl)}`,
});
pushOk({ resource: 'site', operation: 'deleteSitemap', siteUrl, sitemapUrl, success: true, message: 'Sitemap deleted successfully' });
}
}
catch (error) {
pushErr(error);
}
}
return [returnData];
}
}
exports.GoogleSearchConsole = GoogleSearchConsole;