UNPKG

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
"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;