@gscdump/engine-gsc-api 3.4.4 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,65 +4,66 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@gscdump/engine-gsc-api?color=yellow)](https://npm.chart.dev/@gscdump/engine-gsc-api)
5
5
  [![license](https://img.shields.io/github/license/harlan-zw/gscdump?color=yellow)](https://github.com/harlan-zw/gscdump/blob/main/LICENSE)
6
6
 
7
- > GSC live-API engine adapter — wraps the Search Analytics REST API as an `AnalysisQuerySource` for typed analyzer dispatch.
8
-
9
- Wraps the Google Search Console live REST API as a `RowQuerySource` so row-based analyzers (`striking-distance`, `opportunity`, `movers`, `decay`, `brand`, `clustering`, `concentration`, `seasonality`) dispatch through the same `runAnalyzerFromSource` pipeline as engine-backed sources.
10
-
11
- Use this when you have a GSC OAuth token but no synced parquet data — free-tier flows, demo pages, queries whose date range falls outside the synced window. Pair with `createCompositeSource` to fall back to GSC for out-of-range queries.
7
+ Use Google Search Console as an `AnalysisQuerySource`.
8
+ This adapter runs Analyzers with row plans without a local Store.
12
9
 
13
10
  ## Install
14
11
 
15
12
  ```bash
16
- npm install @gscdump/engine-gsc-api @gscdump/engine gscdump
13
+ npm install @gscdump/engine-gsc-api @gscdump/analysis gscdump
17
14
  ```
18
15
 
19
- ## Usage
16
+ ## Query live rows
20
17
 
21
18
  ```ts
22
- import { analyzeMoversFromSource } from '@gscdump/analysis'
23
19
  import { createGscApiQuerySource } from '@gscdump/engine-gsc-api'
24
20
  import { googleSearchConsole } from 'gscdump'
21
+ import { between, date, gsc, page } from 'gscdump/query'
25
22
 
26
- const client = googleSearchConsole(auth)
23
+ const client = googleSearchConsole({ accessToken: process.env.GSC_ACCESS_TOKEN! })
27
24
  const source = createGscApiQuerySource({ client, siteUrl: 'sc-domain:example.com' })
25
+ const state = gsc.select(page)
26
+ .where(between(date, '2026-08-01', '2026-08-28'))
27
+ .limit(100)
28
+ .getState()
28
29
 
29
- const movers = await analyzeMoversFromSource(source, {
30
- current: { startDate: '2026-04-01', endDate: '2026-04-28' },
31
- previous: { startDate: '2026-03-01', endDate: '2026-03-31' },
32
- })
30
+ const rows = await source.queryRows(state)
31
+ console.log(rows)
33
32
  ```
34
33
 
35
- For host apps that mint short-lived access tokens per request:
34
+ See [`@gscdump/analysis`](../analysis/README.md#sources) for Analyzer dispatch.
36
35
 
37
- ```ts
38
- import { createLiveGscSource } from '@gscdump/engine-gsc-api'
36
+ ## Deferred authentication
39
37
 
40
- const source = createLiveGscSource({
41
- siteUrl,
42
- getAccessToken: () => refreshAccessTokenForUser(userId),
43
- })
44
- ```
38
+ `createLiveGscSource({ siteUrl, getAccessToken })` calls your token function on the first query.
39
+ It reuses the resulting client for that Source's lifetime.
40
+ Create a Source per request if your host manages token refresh between requests.
45
41
 
46
42
  ## Exports
47
43
 
48
- - `createGscApiQuerySource({ client, siteUrl })` — `RowQuerySource` over a `GoogleSearchConsoleClient`.
49
- - `createLiveGscSource({ siteUrl, getAccessToken })` — token-refresh wrapper on top of `createGscApiQuerySource`.
50
- - `canProxyToGsc(state)` — guard for `createCompositeSource`: returns `true` if a `BuilderState` can be answered by GSC's native API (no metric filters, no engine-derived dimensions).
51
- - `fetchGscTopN({ client, siteUrl, dimension, range, limit })` — typed top-N rollup helper.
52
- - `fetchGscDaily({ client, siteUrl, range })` — typed daily timeseries helper.
53
- - `collectGscRows(asyncIterable)` — drain `client.query()` into an array.
54
- - `applyBuilderStatePostProcessing(rows, state)` — post-process row collections for predicates GSC can't push down (metric filters, special operators).
55
- - `GSC_API_CAPABILITIES` — `PlannerCapabilities` for the GSC API surface.
56
-
57
- ## Capabilities
58
-
59
- GSC supports regex pushdown via `INCLUDING_REGEX` / `EXCLUDING_REGEX` filters but has no SQL surface, no comparison joins, no cross-dataset queries, and no engine-derived dimensions (`queryCanonical`, `page_keywords`). Pair with `createCompositeSource({ engine, gsc })` from `@gscdump/analysis/source` to route SQL-shaped queries to the engine and date-out-of-range queries to GSC.
60
-
61
- ## Related
62
-
63
- - [`@gscdump/engine`](../engine) — Source contracts (`RowQuerySource`, `AnalysisQuerySource`).
64
- - [`@gscdump/analysis`](../analysis) — Analyzer instances and portable source factories.
65
- - [`gscdump`](../gscdump) — REST client + query builder.
44
+ | Export | Purpose |
45
+ | --- | --- |
46
+ | `createGscApiQuerySource` | Wrap a Google client as a Source |
47
+ | `createLiveGscSource` | Create a Source with deferred token lookup |
48
+ | `canProxyToGsc` | Check whether query inputs support live routing |
49
+ | `fetchGscTopN` | Read top rows for a dimension and date range |
50
+ | `fetchGscDaily` | Read daily metrics |
51
+ | `runGscSyncSlice` | Read a bounded Search Analytics sync slice |
52
+ | `runGscSearchAppearanceContextSlice` | Read a Search Appearance context slice |
53
+
54
+ ## Limits and fallback
55
+
56
+ The Source supports row queries and regex filters.
57
+ It has no SQL execution, comparison joins, or Engine-derived dimensions such as `queryCanonical`.
58
+ `canProxyToGsc` rejects malformed inputs, prefilters, and Engine-derived dimensions in selections or filters.
59
+ Metric filters remain supported after row collection.
60
+
61
+ `createCompositeSource` from `@gscdump/analysis/source` accepts `{ engine, live, site }`.
62
+ It sends supported queries to Google when stored coverage or dimensions cannot answer them.
63
+ The `site` input supplies sync bounds and optional covered date spans.
64
+ SQL execution uses the Engine.
65
+
66
+ Google's [Search Analytics limits](https://developers.google.com/webmaster-tools/v1/how-tos/all-your-data) still apply.
66
67
 
67
68
  ## License
68
69
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CreateLiveGscSourceOptions, canProxyToGsc, createLiveGscSource } from "./live.mjs";
2
2
  import { FetchTopNOptions, GscDailyRow, GscRange, GscTopNRow, fetchGscDaily, fetchGscTopN } from "./rollup-synth.mjs";
3
- import { GscApiQuerySourceOptions, createGscApiQuerySource } from "./source.mjs";
3
+ import { GSC_API_CAPABILITIES, GscApiQuerySourceOptions, createGscApiQuerySource } from "./source.mjs";
4
4
  import { GscApiRow, RunGscSearchAppearanceContextSliceOptions, RunGscSearchAppearanceContextSliceResult, RunGscSyncSliceOptions, RunGscSyncSliceResult, SearchAppearanceContextGrain, SearchAppearanceContextTable, SyncSliceDimensionFilter, SyncSliceDomainFilter, runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
5
- export { type CreateLiveGscSourceOptions, type FetchTopNOptions, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscTopNRow, type RunGscSearchAppearanceContextSliceOptions, type RunGscSearchAppearanceContextSliceResult, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SearchAppearanceContextGrain, type SearchAppearanceContextTable, type SyncSliceDimensionFilter, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
5
+ export { type CreateLiveGscSourceOptions, type FetchTopNOptions, GSC_API_CAPABILITIES, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscTopNRow, type RunGscSearchAppearanceContextSliceOptions, type RunGscSearchAppearanceContextSliceResult, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SearchAppearanceContextGrain, type SearchAppearanceContextTable, type SyncSliceDimensionFilter, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { fetchGscDaily, fetchGscTopN } from "./rollup-synth.mjs";
2
- import { createGscApiQuerySource } from "./source.mjs";
2
+ import { GSC_API_CAPABILITIES, createGscApiQuerySource } from "./source.mjs";
3
3
  import { canProxyToGsc, createLiveGscSource } from "./live.mjs";
4
4
  import { runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
5
- export { canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
5
+ export { GSC_API_CAPABILITIES, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
package/dist/live.d.mts CHANGED
@@ -2,8 +2,8 @@ import { GoogleSearchConsoleClient } from "gscdump";
2
2
  import { BuilderState } from "gscdump/query";
3
3
  import { SearchType as SearchType$1 } from "@gscdump/engine";
4
4
  import { AnalysisQuerySource } from "@gscdump/engine/source";
5
- declare function canProxyToGsc(state: BuilderState): boolean;
6
- interface CreateLiveGscSourceOptions {
5
+ export declare function canProxyToGsc(state: BuilderState): boolean;
6
+ export interface CreateLiveGscSourceOptions {
7
7
  /** GSC property URL (e.g. `sc-domain:example.com` or `https://example.com/`). */
8
8
  siteUrl: string;
9
9
  /**
@@ -21,5 +21,4 @@ interface CreateLiveGscSourceOptions {
21
21
  */
22
22
  searchType?: SearchType$1;
23
23
  }
24
- declare function createLiveGscSource(opts: CreateLiveGscSourceOptions): AnalysisQuerySource;
25
- export { CreateLiveGscSourceOptions, canProxyToGsc, createLiveGscSource };
24
+ export declare function createLiveGscSource(opts: CreateLiveGscSourceOptions): AnalysisQuerySource;
package/dist/live.mjs CHANGED
@@ -1,9 +1,15 @@
1
1
  import { createGscApiQuerySource } from "./source.mjs";
2
2
  import { googleSearchConsole } from "gscdump";
3
+ import { normalizeBuilderStateResult } from "gscdump/query";
3
4
  const PRO_ONLY_DIMENSIONS = /* @__PURE__ */ new Set(["queryCanonical", "page_keywords"]);
5
+ function hasMatchingFilter(filter, matches) {
6
+ return !!filter && (filter._filters.some((leaf) => matches(leaf.dimension)) || (filter._nestedGroups ?? []).some((group) => hasMatchingFilter(group, matches)));
7
+ }
4
8
  function canProxyToGsc(state) {
5
- if (state.dimensions.some((d) => PRO_ONLY_DIMENSIONS.has(d))) return false;
6
- return true;
9
+ const parsed = normalizeBuilderStateResult(state);
10
+ if (!parsed.ok) return false;
11
+ const normalized = parsed.value;
12
+ return !hasMatchingFilter(normalized.prefilter, () => true) && !normalized.dimensions.some((d) => PRO_ONLY_DIMENSIONS.has(d)) && !hasMatchingFilter(normalized.filter, (dimension) => PRO_ONLY_DIMENSIONS.has(dimension));
7
13
  }
8
14
  function withSearchType(state, searchType) {
9
15
  return state.searchType ? state : {
@@ -14,7 +20,10 @@ function withSearchType(state, searchType) {
14
20
  function createLiveGscSource(opts) {
15
21
  let clientPromise = null;
16
22
  function getClient() {
17
- if (!clientPromise) clientPromise = opts.getAccessToken().then((accessToken) => opts.createClient?.(accessToken) ?? googleSearchConsole({ accessToken }));
23
+ if (!clientPromise) clientPromise = opts.getAccessToken().then((accessToken) => opts.createClient?.(accessToken) ?? googleSearchConsole({ accessToken })).catch((error) => {
24
+ clientPromise = null;
25
+ throw error;
26
+ });
18
27
  return clientPromise;
19
28
  }
20
29
  return {
@@ -1,5 +1,5 @@
1
- import { dimensionValue, matchesTopLevelPage, metricValue } from "@gscdump/engine/resolver";
2
1
  import { extractMetricFilters, extractSpecialOperatorFilters } from "gscdump/query";
2
+ import { dimensionValue, matchesTopLevelPage, metricValue } from "@gscdump/engine/resolver";
3
3
  import { normalizeUrl } from "gscdump/normalize";
4
4
  const METRIC_NAMES = [
5
5
  "clicks",
@@ -1,16 +1,16 @@
1
1
  import { GoogleSearchConsoleClient } from "gscdump";
2
2
  import { Column, Dimension, SearchType } from "gscdump/query";
3
- interface GscRange {
3
+ export interface GscRange {
4
4
  start: string;
5
5
  end: string;
6
6
  }
7
- interface GscTopNRow {
7
+ export interface GscTopNRow {
8
8
  key: string;
9
9
  clicks: number;
10
10
  impressions: number;
11
11
  sum_position: number;
12
12
  }
13
- interface FetchTopNOptions<D extends Dimension> {
13
+ export interface FetchTopNOptions<D extends Dimension> {
14
14
  client: GoogleSearchConsoleClient;
15
15
  siteUrl: string;
16
16
  dimension: Column<D>;
@@ -27,19 +27,18 @@ interface FetchTopNOptions<D extends Dimension> {
27
27
  /** GSC search corpus; callers default API-boundary omissions to web. */
28
28
  searchType?: SearchType;
29
29
  }
30
- declare function fetchGscTopN<D extends Dimension>(opts: FetchTopNOptions<D>): Promise<GscTopNRow[]>;
31
- interface GscDailyRow {
30
+ export declare function fetchGscTopN<D extends Dimension>(opts: FetchTopNOptions<D>): Promise<GscTopNRow[]>;
31
+ export interface GscDailyRow {
32
32
  date: number;
33
33
  clicks: number;
34
34
  impressions: number;
35
35
  sum_position: number;
36
36
  anonymizedImpressionsPct: number;
37
37
  }
38
- declare function fetchGscDaily(opts: {
38
+ export declare function fetchGscDaily(opts: {
39
39
  client: GoogleSearchConsoleClient;
40
40
  siteUrl: string;
41
41
  range: GscRange;
42
42
  /** GSC search corpus; callers default API-boundary omissions to web. */
43
43
  searchType?: SearchType;
44
- }): Promise<GscDailyRow[]>;
45
- export { FetchTopNOptions, GscDailyRow, GscRange, GscTopNRow, fetchGscDaily, fetchGscTopN };
44
+ }): Promise<GscDailyRow[]>;
package/dist/source.d.mts CHANGED
@@ -1,8 +1,16 @@
1
1
  import { GoogleSearchConsoleClient } from "gscdump";
2
+ import { PlannerCapabilities } from "gscdump/query/plan";
2
3
  import { AnalysisQuerySource } from "@gscdump/engine/source";
3
- interface GscApiQuerySourceOptions {
4
+ /**
5
+ * Capabilities the live GSC API can satisfy. Regex pushes down via the
6
+ * `INCLUDING_REGEX` / `EXCLUDING_REGEX` filter types; comparison joins and
7
+ * cross-dataset queries do not exist on the wire, and the API does not
8
+ * expose window aggregations. Metric filters and ordering are honored by
9
+ * the source-layer post-process pass after row collection.
10
+ */
11
+ export declare const GSC_API_CAPABILITIES: PlannerCapabilities;
12
+ export interface GscApiQuerySourceOptions {
4
13
  client: GoogleSearchConsoleClient;
5
14
  siteUrl: string;
6
15
  }
7
- declare function createGscApiQuerySource(options: GscApiQuerySourceOptions): AnalysisQuerySource;
8
- export { GscApiQuerySourceOptions, createGscApiQuerySource };
16
+ export declare function createGscApiQuerySource(options: GscApiQuerySourceOptions): AnalysisQuerySource;
package/dist/source.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { applyBuilderStatePostProcessing } from "./post-process.mjs";
2
2
  import { collectRows } from "./rollup-synth.mjs";
3
- import { assertDimensionsSupported, getFilterDimensions } from "@gscdump/engine/resolver";
4
- import { extractMetricFilters, extractSpecialOperatorFilters } from "gscdump/query";
3
+ import { UnsupportedLogicalCapabilityError, extractMetricFilters, extractSpecialOperatorFilters, normalizeFilter } from "gscdump/query";
4
+ import { assertDimensionsSupported, getFilterDimensions, getInternalFilters } from "@gscdump/engine/resolver";
5
5
  import { buildLogicalPlan } from "gscdump/query/plan";
6
6
  const GSC_API_CAPABILITIES = {
7
7
  regex: true,
@@ -30,6 +30,7 @@ function createGscApiQuerySource(options) {
30
30
  kind: "live",
31
31
  capabilities: GSC_API_CAPABILITIES,
32
32
  async queryRows(state) {
33
+ if (getInternalFilters(normalizeFilter(state.prefilter)).length > 0) throw new UnsupportedLogicalCapabilityError("prefilter", "gsc-api query source");
33
34
  buildLogicalPlan(state, GSC_API_CAPABILITIES);
34
35
  const filterDims = getFilterDimensions(state.filter, isMetricDimension);
35
36
  assertDimensionsSupported([...state.dimensions, ...filterDims], "api", "gsc-api query source");
@@ -1,26 +1,26 @@
1
1
  import { GoogleSearchConsoleClient } from "gscdump";
2
2
  import { SearchType } from "@gscdump/engine";
3
3
  import { GscDataState, GscSearchAnalyticsMetadata } from "gscdump/contracts";
4
- interface GscApiRow {
4
+ export interface GscApiRow {
5
5
  keys: string[];
6
6
  clicks: number;
7
7
  impressions: number;
8
8
  ctr: number;
9
9
  position: number;
10
10
  }
11
- interface SyncSliceDomainFilter {
11
+ export interface SyncSliceDomainFilter {
12
12
  /**
13
13
  * Domain (eTLD+1 + subdomain) to scope the slice to — matches both
14
14
  * `www.` and bare variants. Strip the protocol; the regex is built here.
15
15
  */
16
16
  domain?: string;
17
17
  }
18
- interface SyncSliceDimensionFilter {
18
+ export interface SyncSliceDimensionFilter {
19
19
  dimension: 'page' | 'query' | 'country' | 'device' | 'searchAppearance';
20
20
  operator?: 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
21
21
  expression: string;
22
22
  }
23
- interface RunGscSyncSliceOptions {
23
+ export interface RunGscSyncSliceOptions {
24
24
  client: GoogleSearchConsoleClient;
25
25
  siteUrl: string;
26
26
  /** One of the engine sync-fan tables. Drives the dimension list. */
@@ -68,7 +68,7 @@ interface RunGscSyncSliceOptions {
68
68
  rowsThisPage: number;
69
69
  }) => void;
70
70
  }
71
- interface RunGscSyncSliceResult {
71
+ export interface RunGscSyncSliceResult {
72
72
  totalRows: number;
73
73
  hasMore: boolean;
74
74
  nextStartRow: number;
@@ -79,9 +79,9 @@ interface RunGscSyncSliceResult {
79
79
  */
80
80
  metadata?: GscSearchAnalyticsMetadata;
81
81
  }
82
- type SearchAppearanceContextGrain = 'page' | 'query' | 'page_query';
83
- type SearchAppearanceContextTable = 'search_appearance_pages' | 'search_appearance_queries' | 'search_appearance_page_queries';
84
- interface RunGscSearchAppearanceContextSliceOptions {
82
+ export type SearchAppearanceContextGrain = 'page' | 'query' | 'page_query';
83
+ export type SearchAppearanceContextTable = 'search_appearance_pages' | 'search_appearance_queries' | 'search_appearance_page_queries';
84
+ export interface RunGscSearchAppearanceContextSliceOptions {
85
85
  client: GoogleSearchConsoleClient;
86
86
  siteUrl: string;
87
87
  startDate: string;
@@ -110,7 +110,7 @@ interface RunGscSearchAppearanceContextSliceOptions {
110
110
  }) => void;
111
111
  continuation?: SearchAppearanceContinuation;
112
112
  }
113
- type SearchAppearanceContinuation = {
113
+ export type SearchAppearanceContinuation = {
114
114
  phase: 'discovery';
115
115
  appearances: string[];
116
116
  nextStartRow: number;
@@ -120,17 +120,16 @@ type SearchAppearanceContinuation = {
120
120
  appearanceIndex: number;
121
121
  nextStartRow: number;
122
122
  };
123
- interface RunGscSearchAppearanceContextSliceResult {
123
+ export interface RunGscSearchAppearanceContextSliceResult {
124
124
  appearances: string[];
125
125
  totalRows: number;
126
126
  hasMore: boolean;
127
127
  continuation?: SearchAppearanceContinuation;
128
128
  }
129
- declare function runGscSyncSlice(opts: RunGscSyncSliceOptions): Promise<RunGscSyncSliceResult>;
129
+ export declare function runGscSyncSlice(opts: RunGscSyncSliceOptions): Promise<RunGscSyncSliceResult>;
130
130
  /**
131
131
  * Implements GSC's required two-step search-appearance flow:
132
132
  * 1. group by `searchAppearance` alone to discover available appearances;
133
133
  * 2. for each appearance, filter by it and fetch page/query/date context.
134
134
  */
135
- declare function runGscSearchAppearanceContextSlice(opts: RunGscSearchAppearanceContextSliceOptions): Promise<RunGscSearchAppearanceContextSliceResult>;
136
- export { GscApiRow, RunGscSearchAppearanceContextSliceOptions, RunGscSearchAppearanceContextSliceResult, RunGscSyncSliceOptions, RunGscSyncSliceResult, SearchAppearanceContextGrain, SearchAppearanceContextTable, SearchAppearanceContinuation, SyncSliceDimensionFilter, SyncSliceDomainFilter, runGscSearchAppearanceContextSlice, runGscSyncSlice };
135
+ export declare function runGscSearchAppearanceContextSlice(opts: RunGscSearchAppearanceContextSliceOptions): Promise<RunGscSearchAppearanceContextSliceResult>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine-gsc-api",
3
3
  "type": "module",
4
- "version": "3.4.4",
4
+ "version": "3.6.0",
5
5
  "description": "GSC live-API engine adapter — wraps the Search Analytics REST API as an AnalysisQuerySource for typed analyzer dispatch.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -36,11 +36,11 @@
36
36
  "node": ">=22"
37
37
  },
38
38
  "dependencies": {
39
- "@gscdump/engine": "^3.4.4",
40
- "gscdump": "^3.4.4"
39
+ "@gscdump/engine": "^3.6.0",
40
+ "gscdump": "^3.6.0"
41
41
  },
42
42
  "devDependencies": {
43
- "vitest": "^4.1.11"
43
+ "vitest": "^5.0.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "obuild",