UNPKG

n8n-nodes-google-search-console

Version:
625 lines (624 loc) 32.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GoogleSearchConsole = void 0; const n8n_workflow_1 = require("n8n-workflow"); 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; } 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 ctx.helpers.httpRequestWithAuthentication.call(ctx, 'GoogleSearchConsoleOAuth2Api', { 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); } function keyFromRow(dimensions, row) { var _a; return ((_a = row.keys) !== null && _a !== void 0 ? _a : []).join('||'); } function rowsToMap(rows, dims) { const m = new Map(); for (const r of rows) m.set(keyFromRow(dims, r), r); return m; } function daysInclusive(a, b) { const start = new Date(a); const end = new Date(b); const msPerDay = 24 * 60 * 60 * 1000; const startUTC = Date.UTC(start.getFullYear(), start.getMonth(), start.getDate()); const endUTC = Date.UTC(end.getFullYear(), end.getMonth(), end.getDate()); return Math.floor((endUTC - startUTC) / msPerDay) + 1; } function shiftDays(iso, days) { const d = new Date(iso); d.setDate(d.getDate() + days); return toGscDate(d.toISOString()); } function shiftYears(iso, years) { const d = new Date(iso); d.setFullYear(d.getFullYear() + years); return toGscDate(d.toISOString()); } function buildCompareRanges(mode, rangeA, customB) { if (mode === 'prevPeriod') { const dur = daysInclusive(rangeA.startDate, rangeA.endDate); const endB = shiftDays(rangeA.startDate, -1); const startB = shiftDays(endB, -(dur - 1)); return { rangeA, rangeB: { startDate: startB, endDate: endB } }; } if (mode === 'prevYear') { const startB = shiftYears(rangeA.startDate, -1); const endB = shiftYears(rangeA.endDate, -1); return { rangeA, rangeB: { startDate: startB, endDate: endB } }; } const { startDate, endDate } = rangeFromPreset((customB === null || customB === void 0 ? void 0 : customB.mode) || 'last28d', customB === null || customB === void 0 ? void 0 : customB.start, customB === null || customB === void 0 ? void 0 : customB.end); return { rangeA, rangeB: { startDate, endDate } }; } function validateSiteUrlOrThrow(node, itemIndex, siteUrl, context) { const trimmed = (siteUrl || '').trim(); if (!trimmed || trimmed === '__NO_SITES__') { throw new n8n_workflow_1.NodeOperationError(node.getNode(), `No site selected for ${context}.`, { itemIndex }); } if (!/^https?:\/\//.test(trimmed) && !/^sc-domain:/.test(trimmed)) { throw new n8n_workflow_1.NodeOperationError(node.getNode(), 'Site URL must start with http(s):// or sc-domain:. Example: https://example.com/ or sc-domain:example.com', { itemIndex }); } return trimmed; } /* ========= Node ========= */ class GoogleSearchConsole { constructor() { this.description = { displayName: 'Google Search Console', name: 'googleSearchConsole', icon: 'file:googlesearchconsole.svg', group: ['resource'], version: 1, description: 'Connect to Google Search Console API', usableAsTool: true, defaults: { name: 'Search Console' }, subtitle: '={{$parameter.operation}}', inputs: ['main'], outputs: ['main'], credentials: [{ name: 'GoogleSearchConsoleOAuth2Api', required: true }], 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: 'Compare Page Insights', value: 'comparePageInsights', action: 'Compare search analytics between two date ranges' }, ], 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 a verified property URL. Supports domain properties via sc-domain:example.com', 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', hint: 'Supports sc-domain:example.com for domain properties', 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: '', }, /* ---------- comparePageInsights ---------- */ { displayName: 'Site URL Mode', name: 'siteUrlModeCompare', type: 'options', options: [ { name: 'Pick from My Verified Sites', value: 'list' }, { name: 'Enter Manually', value: 'manual' }, ], default: 'list', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'] } }, }, { displayName: 'Site URL', name: 'siteUrlCompare', type: 'options', typeOptions: { loadOptionsMethod: 'getVerifiedSites' }, displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'], siteUrlModeCompare: ['list'] }, }, default: '', required: true, }, { displayName: 'Site URL (Manual)', name: 'siteUrlCompareManual', type: 'string', placeholder: 'https://example.com/ or sc-domain:example.com', hint: 'Enter a verified property URL (supports sc-domain:example.com)', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'], siteUrlModeCompare: ['manual'] }, }, default: '', required: true, }, /* ---- Range A ---- */ { displayName: 'Date Range A', name: 'dateRangeModeA', 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', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'] } }, }, { displayName: 'Start Date A', name: 'startDateA', type: 'dateTime', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'], dateRangeModeA: ['custom'] } }, default: '', }, { displayName: 'End Date A', name: 'endDateA', type: 'dateTime', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'], dateRangeModeA: ['custom'] } }, default: '', }, { displayName: 'Compare Mode', name: 'compareMode', type: 'options', options: [ { name: 'Previous Period', value: 'prevPeriod' }, { name: 'Previous Year (YoY)', value: 'prevYear' }, { name: 'Custom', value: 'custom' }, ], default: 'prevPeriod', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'] } }, }, /* ---- Range B (only when compare is custom) ---- */ { displayName: 'Date Range B', name: 'dateRangeModeB', 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', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'], compareMode: ['custom'] } }, }, { displayName: 'Start Date B', name: 'startDateB', type: 'dateTime', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'], compareMode: ['custom'], dateRangeModeB: ['custom'] }, }, default: '', }, { displayName: 'End Date B', name: 'endDateB', type: 'dateTime', displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'], compareMode: ['custom'], dateRangeModeB: ['custom'] }, }, default: '', }, /* ---- Shared options ---- */ { displayName: 'Row Limit (per range)', name: 'rowLimitCompare', type: 'number', typeOptions: { minValue: 1, maxValue: 25000 }, default: 1000, displayOptions: { show: { resource: ['site'], operation: ['comparePageInsights'] } }, }, { displayName: 'Search Type', name: 'searchTypeCompare', 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: ['comparePageInsights'] } }, }, { displayName: 'Dimensions', name: 'dimensionsCompare', 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: ['comparePageInsights'] } }, }, ], }; this.methods = { loadOptions: { async getVerifiedSites() { try { const resp = await this.helpers.httpRequestWithAuthentication.call(this, 'GoogleSearchConsoleOAuth2Api', { 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 — verify your site in Google Search Console first.', 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 loading sites: ${(err === null || err === void 0 ? void 0 : err.message) || 'Failed to load sites. Check your OAuth credentials/permissions.'}`, value: '__NO_SITES__', }]; } }, }, }; } async execute() { var _a, _b, _c, _d, _e, _f; 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 this.helpers.httpRequestWithAuthentication.call(this, 'GoogleSearchConsoleOAuth2Api', { 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 siteUrlInput = siteUrlMode === 'manual' ? this.getNodeParameter('siteUrlManual', i) : this.getNodeParameter('siteUrl', i); const siteUrl = validateSiteUrlOrThrow(this, i, siteUrlInput, 'page insights'); 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 siteUrlInput = siteUrlMode === 'manual' ? this.getNodeParameter('inspectSiteUrlManual', i) : this.getNodeParameter('inspectSiteUrl', i); const siteUrl = validateSiteUrlOrThrow(this, i, siteUrlInput, 'URL inspection'); const inspectionUrl = this.getNodeParameter('inspectionUrl', i).trim(); if (!/^https?:\/\//.test(inspectionUrl)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Inspection URL must start with http(s)://', { itemIndex: i }); } const languageCode = this.getNodeParameter('languageCode', i) || ''; const resp = await this.helpers.httpRequestWithAuthentication.call(this, 'GoogleSearchConsoleOAuth2Api', { 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 }); } /* ---------- Compare Page Insights ---------- */ if (operation === 'comparePageInsights') { const mode = this.getNodeParameter('siteUrlModeCompare', i) || 'list'; const siteUrlInput = mode === 'manual' ? this.getNodeParameter('siteUrlCompareManual', i) : this.getNodeParameter('siteUrlCompare', i); const siteUrl = validateSiteUrlOrThrow(this, i, siteUrlInput, 'comparison'); const dims = this.getNodeParameter('dimensionsCompare', i) || ['page']; if (!dims.length) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'At least one dimension is required for comparison.', { itemIndex: i }); } // Range A (preset/custom) const drmA = this.getNodeParameter('dateRangeModeA', i) || 'last28d'; const startA = this.getNodeParameter('startDateA', i, '') || ''; const endA = this.getNodeParameter('endDateA', i, '') || ''; const rangeA = rangeFromPreset(drmA, startA, endA); // Compare mode: prevPeriod / prevYear / custom const compareMode = this.getNodeParameter('compareMode', i) || 'prevPeriod'; // اگر Custom انتخاب شده، پارامترهای B را بخوان let customB; if (compareMode === 'custom') { const drmB = this.getNodeParameter('dateRangeModeB', i) || 'last28d'; const startB = this.getNodeParameter('startDateB', i, '') || ''; const endB = this.getNodeParameter('endDateB', i, '') || ''; customB = { mode: drmB, start: startB, end: endB }; } const { rangeB } = buildCompareRanges(compareMode, rangeA, customB); const rowLimit = (this.getNodeParameter('rowLimitCompare', i) || 1000); const searchType = this.getNodeParameter('searchTypeCompare', i) || 'web'; const baseBody = { dimensions: dims, searchType }; const rowsA = await fetchAllRows(this, siteUrl, { ...baseBody, ...rangeA, rowLimit: Math.max(1, Math.min(rowLimit, 25000)) }, rowLimit); const rowsB = await fetchAllRows(this, siteUrl, { ...baseBody, ...rangeB, rowLimit: Math.max(1, Math.min(rowLimit, 25000)) }, rowLimit); const mapA = rowsToMap(rowsA, dims); const mapB = rowsToMap(rowsB, dims); const allKeys = new Set([...mapA.keys(), ...mapB.keys()]); for (const k of allKeys) { const ra = mapA.get(k); const rb = mapB.get(k); const valsA = ra !== null && ra !== void 0 ? ra : { keys: ((_b = (_a = ra === null || ra === void 0 ? void 0 : ra.keys) !== null && _a !== void 0 ? _a : rb === null || rb === void 0 ? void 0 : rb.keys) !== null && _b !== void 0 ? _b : []), clicks: 0, impressions: 0, ctr: 0, position: 0 }; const valsB = rb !== null && rb !== void 0 ? rb : { keys: ((_d = (_c = rb === null || rb === void 0 ? void 0 : rb.keys) !== null && _c !== void 0 ? _c : ra === null || ra === void 0 ? void 0 : ra.keys) !== null && _d !== void 0 ? _d : []), clicks: 0, impressions: 0, ctr: 0, position: 0 }; const out = {}; ((_f = (_e = valsA.keys) !== null && _e !== void 0 ? _e : valsB.keys) !== null && _f !== void 0 ? _f : []).forEach((v, idx) => { out[dims[idx]] = v; }); out.clicks_a = valsA.clicks; out.clicks_b = valsB.clicks; out.clicks_diff = valsA.clicks - valsB.clicks; out.impr_a = valsA.impressions; out.impr_b = valsB.impressions; out.impr_diff = valsA.impressions - valsB.impressions; out.ctr_a = valsA.ctr; out.ctr_b = valsB.ctr; out.ctr_diff = valsA.ctr - valsB.ctr; out.pos_a = valsA.position; out.pos_b = valsB.position; out.pos_diff = valsA.position - valsB.position; out.range_a = rangeA; // { startDate, endDate } out.range_b = rangeB; out.compare_mode = compareMode; returnData.push({ json: { resource: 'site', operation: 'comparePageInsights', ...out }, pairedItem: { item: i } }); } } } catch (error) { pushErr(error); } } return [returnData]; } } exports.GoogleSearchConsole = GoogleSearchConsole;