gscdump 0.25.14 → 0.26.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.
@@ -169,6 +169,87 @@ declare const impressions: MetricColumn<"impressions">;
169
169
  declare const ctr: MetricColumn<"ctr">;
170
170
  declare const position: MetricColumn<"position">;
171
171
  declare const searchType: QueryParam<"searchType">;
172
+ type QueryErrorKind = 'missing-date-range' | 'invalid-row-limit' | 'invalid-start-row' | 'invalid-data-state' | 'invalid-aggregation-type' | 'invalid-builder-state' | 'unsupported-capability' | 'unresolvable-dataset';
173
+ type QueryError = {
174
+ kind: 'missing-date-range';
175
+ message: string;
176
+ } | {
177
+ kind: 'invalid-row-limit';
178
+ value: unknown;
179
+ message: string;
180
+ } | {
181
+ kind: 'invalid-start-row';
182
+ value: unknown;
183
+ message: string;
184
+ } | {
185
+ kind: 'invalid-data-state';
186
+ message: string;
187
+ } | {
188
+ kind: 'invalid-aggregation-type';
189
+ message: string;
190
+ } | {
191
+ kind: 'invalid-builder-state';
192
+ message: string;
193
+ cause?: unknown;
194
+ } | {
195
+ kind: 'unsupported-capability';
196
+ capability: string;
197
+ context: string;
198
+ message: string;
199
+ } | {
200
+ kind: 'unresolvable-dataset';
201
+ dimensions: readonly Dimension[];
202
+ filterDims: readonly Dimension[];
203
+ message: string;
204
+ };
205
+ declare const queryErrors: {
206
+ readonly missingDateRange: () => QueryError;
207
+ readonly invalidRowLimit: (value: unknown) => QueryError;
208
+ readonly invalidStartRow: (value: unknown) => QueryError;
209
+ readonly hourDimensionRequiresHourlyState: () => QueryError;
210
+ readonly hourlyStateRequiresHourDimension: () => QueryError;
211
+ readonly byPropertyUnsupportedSearchType: () => QueryError;
212
+ readonly byPropertyNotAllowedWithPage: () => QueryError;
213
+ readonly byNewsShowcaseRequiresSearchType: () => QueryError;
214
+ readonly byNewsShowcaseNotAllowedWithPage: () => QueryError;
215
+ readonly byNewsShowcaseRequiresShowcaseFilter: () => QueryError;
216
+ readonly invalidBuilderState: (cause?: unknown) => QueryError;
217
+ readonly unsupportedCapability: (capability: string, context: string) => QueryError;
218
+ readonly unresolvableDataset: (dimensions: readonly Dimension[], filterDims?: readonly Dimension[]) => QueryError;
219
+ };
220
+ declare function isQueryError(value: unknown): value is QueryError;
221
+ /** The human-readable rendering of a `QueryError`, for logs and string sinks. */
222
+ declare function formatQueryError(error: QueryError): string;
223
+ /**
224
+ * Thrown when a query needs a planner capability (regex pushdown, comparison
225
+ * joins, multi-dataset reads) the target engine lacks. Engines catch it to fall
226
+ * back to the live GSC API. Carries the typed `queryError` value so a caller can
227
+ * read the modelled failure instead of parsing the message.
228
+ */
229
+ declare class UnsupportedLogicalCapabilityError extends Error {
230
+ readonly queryError: Extract<QueryError, {
231
+ kind: 'unsupported-capability';
232
+ }>;
233
+ constructor(capability: string, context: string);
234
+ }
235
+ /**
236
+ * Thrown when a query's grouped + filtered dimensions span more than one stored
237
+ * dataset. Replaces the resolver's raw "unknown column" error so hosts can map
238
+ * it to a 4xx instead of leaking an opaque 500. Carries the typed `queryError`.
239
+ */
240
+ declare class UnresolvableDatasetError extends Error {
241
+ readonly queryError: Extract<QueryError, {
242
+ kind: 'unresolvable-dataset';
243
+ }>;
244
+ constructor(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]);
245
+ }
246
+ /**
247
+ * Re-raises a `QueryError` value as an `Error`, preserving the historical class
248
+ * identity for the two errors engines match on by name (`UnresolvableDatasetError`,
249
+ * `UnsupportedLogicalCapabilityError`). Pairs with `unwrapResult` so a
250
+ * `fooResult(): Result<A, QueryError>` core can back a throwing `foo()`.
251
+ */
252
+ declare function queryErrorToException(error: QueryError): Error;
172
253
  declare function eq<D extends Dimension, V extends DimensionValueMap[D]>(column: Column<D>, value: V): Filter<Record<D, V>>;
173
254
  declare function eq<P extends QueryParamName, V extends QueryParamValueMap[P]>(param: QueryParam<P>, value: V): Filter<Record<P, V>>;
174
255
  declare function ne<D extends Dimension>(column: Column<D>, value: DimensionValueMap[D]): Filter<object>;
@@ -191,6 +272,15 @@ declare function lt(column: Column<'date'>, value: string): Filter<object>;
191
272
  declare function between<M extends Metric>(column: MetricColumn<M>, start: number, end: number): Filter<object>;
192
273
  declare function between(column: Column<'date'>, start: string, end: string): Filter<object>;
193
274
  declare function topLevel(column: Column<'page'>): Filter<object>;
275
+ interface Ok<A> {
276
+ readonly ok: true;
277
+ readonly value: A;
278
+ }
279
+ interface Err<E> {
280
+ readonly ok: false;
281
+ readonly error: E;
282
+ }
283
+ type Result<A, E> = Ok<A> | Err<E>;
194
284
  type LogicalDataset = TableName;
195
285
  type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
196
286
  interface PlannerCapabilities {
@@ -248,14 +338,31 @@ interface LogicalComparisonPlan {
248
338
  previous: LogicalQueryPlan;
249
339
  comparisonFilter?: ComparisonFilter;
250
340
  }
251
- declare class UnsupportedLogicalCapabilityError extends Error {
252
- constructor(capability: keyof PlannerCapabilities, context: string);
253
- }
341
+ /**
342
+ * Errors-as-values core: builds the logical plan or returns a typed `QueryError`
343
+ * for every modelled failure (missing date range, a regex filter on an engine
344
+ * without regex pushdown, a cross-dimension query with no stored home).
345
+ * `buildLogicalPlan` is the throwing wrapper for call sites that prefer exceptions.
346
+ */
347
+ declare function buildLogicalPlanResult(state: BuilderState, capabilities?: PlannerCapabilities): Result<LogicalQueryPlan, QueryError>;
254
348
  declare function buildLogicalPlan(state: BuilderState, capabilities?: PlannerCapabilities): LogicalQueryPlan;
349
+ /**
350
+ * Errors-as-values core for the comparison plan: returns a typed `QueryError`
351
+ * when the engine lacks the comparison-join or multi-dataset capability the
352
+ * paired queries need, or when either side fails to plan.
353
+ */
354
+ declare function buildLogicalComparisonPlanResult(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): Result<LogicalComparisonPlan, QueryError>;
255
355
  declare function buildLogicalComparisonPlan(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): LogicalComparisonPlan;
256
356
  declare function isJsonFilter(value: unknown): value is JsonFilter;
257
357
  declare function parseJsonFilter(json: JsonFilter): Filter<any>;
258
358
  declare function normalizeFilter(input?: FilterInput): Filter<any> | undefined;
359
+ /**
360
+ * Errors-as-values core for {@link normalizeBuilderState}: returns an
361
+ * `invalid-builder-state` `QueryError` when the untrusted partner-API body is
362
+ * not an object, instead of throwing. Receive-edge parse, so hosts can map a bad
363
+ * body to a 4xx.
364
+ */
365
+ declare function normalizeBuilderStateResult(state: unknown): Result<BuilderState, QueryError>;
259
366
  declare function normalizeBuilderState(state: unknown): BuilderState;
260
367
  declare function extractDateRange(input?: FilterInput): {
261
368
  startDate?: string;
@@ -270,7 +377,16 @@ declare function extractSpecialOperatorFilters(input?: FilterInput): InternalFil
270
377
  * don't reach the engine.
271
378
  */
272
379
  declare function extractSearchType(state: BuilderState | undefined | null): SearchType | undefined;
380
+ /**
381
+ * Errors-as-values core: turns a `BuilderState` into a GSC API request body or
382
+ * returns a typed `QueryError` for every modelled bad-query case (missing date
383
+ * range, out-of-range row limit / start row, hour/dataState mismatch, illegal
384
+ * aggregationType combination). `resolveToBody` is the throwing wrapper over this
385
+ * for `.toBody()` and the live-API client paths.
386
+ */
387
+ declare function resolveToBodyResult(state: BuilderState): Result<GscSearchAnalyticsRequest, QueryError>;
388
+ declare function resolveToBody(state: BuilderState): GscSearchAnalyticsRequest;
273
389
  declare function currentPstDate(): string;
274
390
  declare function today(): string;
275
391
  declare function daysAgo(n: number): string;
276
- export { type BuilderState, type Column, type ComparisonFilter, Countries, type Country, type Device, Devices, type Dimension, type DimensionValueMap, type Filter, type FilterInput, type GSCQueryBuilder, type GSCResult, type GSCRow, type InternalFilter, type JsonFilter, type JsonInternalFilter, type LogicalComparisonPlan, type LogicalDataset, type LogicalDimensionFilter, type LogicalMetricFilter, type LogicalQueryPlan, type Metric, type MetricColumn, type PlannerCapabilities, type QueryParam, type QueryParamName, type QueryParamValueMap, type SearchType, SearchTypes, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalPlan, clicks, contains, country, ctr, currentPstDate, date, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, gsc, gt, gte, hour, impressions, inArray, isJsonFilter, like, lt, lte, ne, normalizeBuilderState, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, regex, searchAppearance, searchType, today, topLevel };
392
+ export { type BuilderState, type Column, type ComparisonFilter, Countries, type Country, type Device, Devices, type Dimension, type DimensionValueMap, type Filter, type FilterInput, type GSCQueryBuilder, type GSCResult, type GSCRow, type InternalFilter, type JsonFilter, type JsonInternalFilter, type LogicalComparisonPlan, type LogicalDataset, type LogicalDimensionFilter, type LogicalMetricFilter, type LogicalQueryPlan, type Metric, type MetricColumn, type PlannerCapabilities, QueryError, QueryErrorKind, type QueryParam, type QueryParamName, type QueryParamValueMap, type SearchType, SearchTypes, UnresolvableDatasetError, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalComparisonPlanResult, buildLogicalPlan, buildLogicalPlanResult, clicks, contains, country, ctr, currentPstDate, date, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, formatQueryError, gsc, gt, gte, hour, impressions, inArray, isJsonFilter, isQueryError, like, lt, lte, ne, normalizeBuilderState, normalizeBuilderStateResult, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, queryErrorToException, queryErrors, regex, resolveToBody, resolveToBodyResult, searchAppearance, searchType, today, topLevel };
@@ -29,6 +29,22 @@ function toIsoDate(d) {
29
29
  function addDays(dateStr, n) {
30
30
  return toIsoDate(new Date(Date.parse(`${dateStr}T00:00:00Z`) + n * MS_PER_DAY));
31
31
  }
32
+ function ok(value) {
33
+ return {
34
+ ok: true,
35
+ value
36
+ };
37
+ }
38
+ function err(error) {
39
+ return {
40
+ ok: false,
41
+ error
42
+ };
43
+ }
44
+ function unwrapResult(result, toError) {
45
+ if (result.ok) return result.value;
46
+ throw toError(result.error);
47
+ }
32
48
  var countries_default = [
33
49
  {
34
50
  "name": "Afghanistan",
@@ -1539,6 +1555,142 @@ const SearchTypes = {
1539
1555
  GOOGLE_NEWS: "googleNews"
1540
1556
  };
1541
1557
  const Countries = Object.fromEntries(countries_default.map((c) => [c["alpha-3"], c["alpha-3"].toLowerCase()]));
1558
+ const TIME_AXIS_DIMENSIONS$1 = new Set(["date", "hour"]);
1559
+ const queryErrors = {
1560
+ missingDateRange() {
1561
+ return {
1562
+ kind: "missing-date-range",
1563
+ message: "Date range required: use .where(between(date, start, end)) or .where(and(gte(date, start), lte(date, end)))"
1564
+ };
1565
+ },
1566
+ invalidRowLimit(value) {
1567
+ return {
1568
+ kind: "invalid-row-limit",
1569
+ value,
1570
+ message: `rowLimit must be a positive integer, got ${value}`
1571
+ };
1572
+ },
1573
+ invalidStartRow(value) {
1574
+ return {
1575
+ kind: "invalid-start-row",
1576
+ value,
1577
+ message: `startRow must be a non-negative integer, got ${value}`
1578
+ };
1579
+ },
1580
+ hourDimensionRequiresHourlyState() {
1581
+ return {
1582
+ kind: "invalid-data-state",
1583
+ message: "hour dimension requires dataState: \"hourly_all\""
1584
+ };
1585
+ },
1586
+ hourlyStateRequiresHourDimension() {
1587
+ return {
1588
+ kind: "invalid-data-state",
1589
+ message: "dataState: \"hourly_all\" requires grouping by hour dimension"
1590
+ };
1591
+ },
1592
+ byPropertyUnsupportedSearchType() {
1593
+ return {
1594
+ kind: "invalid-aggregation-type",
1595
+ message: "aggregationType: \"byProperty\" is not supported for type \"discover\" or \"googleNews\""
1596
+ };
1597
+ },
1598
+ byPropertyNotAllowedWithPage() {
1599
+ return {
1600
+ kind: "invalid-aggregation-type",
1601
+ message: "aggregationType: \"byProperty\" is not allowed when grouping or filtering by page"
1602
+ };
1603
+ },
1604
+ byNewsShowcaseRequiresSearchType() {
1605
+ return {
1606
+ kind: "invalid-aggregation-type",
1607
+ message: "aggregationType: \"byNewsShowcasePanel\" requires type \"discover\" or \"googleNews\""
1608
+ };
1609
+ },
1610
+ byNewsShowcaseNotAllowedWithPage() {
1611
+ return {
1612
+ kind: "invalid-aggregation-type",
1613
+ message: "aggregationType: \"byNewsShowcasePanel\" is not allowed when grouping or filtering by page"
1614
+ };
1615
+ },
1616
+ byNewsShowcaseRequiresShowcaseFilter() {
1617
+ return {
1618
+ kind: "invalid-aggregation-type",
1619
+ message: "aggregationType: \"byNewsShowcasePanel\" requires a searchAppearance equals \"NEWS_SHOWCASE\" filter and no other searchAppearance filter"
1620
+ };
1621
+ },
1622
+ invalidBuilderState(cause) {
1623
+ return {
1624
+ kind: "invalid-builder-state",
1625
+ cause,
1626
+ message: "Invalid state"
1627
+ };
1628
+ },
1629
+ unsupportedCapability(capability, context) {
1630
+ return {
1631
+ kind: "unsupported-capability",
1632
+ capability,
1633
+ context,
1634
+ message: `${context} requires ${capability} capability`
1635
+ };
1636
+ },
1637
+ unresolvableDataset(dimensions, filterDims = []) {
1638
+ const grouped = dimensions.filter((d) => !TIME_AXIS_DIMENSIONS$1.has(d));
1639
+ const filtered = filterDims.filter((d) => !TIME_AXIS_DIMENSIONS$1.has(d));
1640
+ return {
1641
+ kind: "unresolvable-dataset",
1642
+ dimensions,
1643
+ filterDims,
1644
+ message: `Cannot resolve a [${grouped.join(", ")}] breakdown filtered by [${filtered.join(", ")}] from stored data: these dimensions live in separate per-dimension tables. Only the live GSC API computes cross-dimension aggregates.`
1645
+ };
1646
+ }
1647
+ };
1648
+ const QUERY_ERROR_KINDS = new Set([
1649
+ "missing-date-range",
1650
+ "invalid-row-limit",
1651
+ "invalid-start-row",
1652
+ "invalid-data-state",
1653
+ "invalid-aggregation-type",
1654
+ "invalid-builder-state",
1655
+ "unsupported-capability",
1656
+ "unresolvable-dataset"
1657
+ ]);
1658
+ function isQueryError(value) {
1659
+ return typeof value === "object" && value !== null && QUERY_ERROR_KINDS.has(value.kind) && typeof value.message === "string";
1660
+ }
1661
+ function formatQueryError(error) {
1662
+ return error.message;
1663
+ }
1664
+ var UnsupportedLogicalCapabilityError = class extends Error {
1665
+ queryError;
1666
+ constructor(capability, context) {
1667
+ const error = queryErrors.unsupportedCapability(capability, context);
1668
+ super(error.message);
1669
+ this.name = "UnsupportedLogicalCapabilityError";
1670
+ this.queryError = error;
1671
+ }
1672
+ };
1673
+ var UnresolvableDatasetError = class extends Error {
1674
+ queryError;
1675
+ constructor(dimensions, filterDims = []) {
1676
+ const error = queryErrors.unresolvableDataset(dimensions, filterDims);
1677
+ super(error.message);
1678
+ this.name = "UnresolvableDatasetError";
1679
+ this.queryError = error;
1680
+ }
1681
+ };
1682
+ function queryErrorToException(error) {
1683
+ switch (error.kind) {
1684
+ case "unsupported-capability": return new UnsupportedLogicalCapabilityError(error.capability, error.context);
1685
+ case "unresolvable-dataset": return new UnresolvableDatasetError(error.dimensions, error.filterDims);
1686
+ default: {
1687
+ const exception = new Error(error.message);
1688
+ if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
1689
+ exception.queryError = error;
1690
+ return exception;
1691
+ }
1692
+ }
1693
+ }
1542
1694
  const DATE_OPERATORS = [
1543
1695
  "gte",
1544
1696
  "gt",
@@ -1641,8 +1793,8 @@ function normalizeFilter(input) {
1641
1793
  if (isWireFilter(input)) return convertWireGroup(input) ?? void 0;
1642
1794
  return input;
1643
1795
  }
1644
- function normalizeBuilderState(state) {
1645
- if (!state || typeof state !== "object") throw new Error("Invalid state");
1796
+ function normalizeBuilderStateResult(state) {
1797
+ if (!state || typeof state !== "object") return err(queryErrors.invalidBuilderState(state));
1646
1798
  const s = state;
1647
1799
  const normalized = {
1648
1800
  dimensions: s.dimensions,
@@ -1655,7 +1807,10 @@ function normalizeBuilderState(state) {
1655
1807
  aggregationType: s.aggregationType
1656
1808
  };
1657
1809
  if (typeof s.searchType === "string" && KNOWN_SEARCH_TYPES.has(s.searchType)) normalized.searchType = s.searchType;
1658
- return normalized;
1810
+ return ok(normalized);
1811
+ }
1812
+ function normalizeBuilderState(state) {
1813
+ return unwrapResult(normalizeBuilderStateResult(state), queryErrorToException);
1659
1814
  }
1660
1815
  function extractSpecialFilters(filter) {
1661
1816
  if (!filter) return {};
@@ -1735,9 +1890,9 @@ function extractSearchType(state) {
1735
1890
  if (typeof raw !== "string" || raw.length === 0) return void 0;
1736
1891
  return KNOWN_SEARCH_TYPES.has(raw) ? raw : void 0;
1737
1892
  }
1738
- function resolveToBody(state) {
1893
+ function resolveToBodyResult(state) {
1739
1894
  const { startDate, endDate, searchType, dimensionFilter } = extractSpecialFilters(state.filter);
1740
- if (!startDate || !endDate) throw new Error("Date range required: use .where(between(date, start, end)) or .where(and(gte(date, start), lte(date, end)))");
1895
+ if (!startDate || !endDate) return err(queryErrors.missingDateRange());
1741
1896
  const body = {
1742
1897
  dimensions: state.dimensions,
1743
1898
  startDate,
@@ -1746,16 +1901,16 @@ function resolveToBody(state) {
1746
1901
  const resolvedType = state.searchType ?? searchType;
1747
1902
  if (resolvedType) body.type = resolvedType;
1748
1903
  if (state.rowLimit !== void 0) {
1749
- if (!Number.isInteger(state.rowLimit) || state.rowLimit < 1) throw new Error(`rowLimit must be a positive integer, got ${state.rowLimit}`);
1904
+ if (!Number.isInteger(state.rowLimit) || state.rowLimit < 1) return err(queryErrors.invalidRowLimit(state.rowLimit));
1750
1905
  body.rowLimit = state.rowLimit;
1751
1906
  }
1752
1907
  if (state.startRow !== void 0) {
1753
- if (!Number.isInteger(state.startRow) || state.startRow < 0) throw new Error(`startRow must be a non-negative integer, got ${state.startRow}`);
1908
+ if (!Number.isInteger(state.startRow) || state.startRow < 0) return err(queryErrors.invalidStartRow(state.startRow));
1754
1909
  if (state.startRow > 0) body.startRow = state.startRow;
1755
1910
  }
1756
1911
  const hasHour = state.dimensions?.includes("hour");
1757
- if (hasHour && state.dataState !== "hourly_all") throw new Error("hour dimension requires dataState: \"hourly_all\"");
1758
- if (state.dataState === "hourly_all" && !hasHour) throw new Error("dataState: \"hourly_all\" requires grouping by hour dimension");
1912
+ if (hasHour && state.dataState !== "hourly_all") return err(queryErrors.hourDimensionRequiresHourlyState());
1913
+ if (state.dataState === "hourly_all" && !hasHour) return err(queryErrors.hourlyStateRequiresHourDimension());
1759
1914
  if (state.dataState) body.dataState = state.dataState;
1760
1915
  const filterGroups = resolveFilter(dimensionFilter);
1761
1916
  if (filterGroups.length > 0) body.dimensionFilterGroups = filterGroups;
@@ -1764,20 +1919,23 @@ function resolveToBody(state) {
1764
1919
  const apiLeafFilters = filterGroups.flatMap((g) => g.filters ?? []);
1765
1920
  const filtersByPage = apiLeafFilters.some((f) => f.dimension === "page");
1766
1921
  if (state.aggregationType === "byProperty") {
1767
- if (body.type === "discover" || body.type === "googleNews") throw new Error("aggregationType: \"byProperty\" is not supported for type \"discover\" or \"googleNews\"");
1768
- if (groupsByPage || filtersByPage) throw new Error("aggregationType: \"byProperty\" is not allowed when grouping or filtering by page");
1922
+ if (body.type === "discover" || body.type === "googleNews") return err(queryErrors.byPropertyUnsupportedSearchType());
1923
+ if (groupsByPage || filtersByPage) return err(queryErrors.byPropertyNotAllowedWithPage());
1769
1924
  }
1770
1925
  if (state.aggregationType === "byNewsShowcasePanel") {
1771
- if (body.type !== "discover" && body.type !== "googleNews") throw new Error("aggregationType: \"byNewsShowcasePanel\" requires type \"discover\" or \"googleNews\"");
1772
- if (groupsByPage || filtersByPage) throw new Error("aggregationType: \"byNewsShowcasePanel\" is not allowed when grouping or filtering by page");
1926
+ if (body.type !== "discover" && body.type !== "googleNews") return err(queryErrors.byNewsShowcaseRequiresSearchType());
1927
+ if (groupsByPage || filtersByPage) return err(queryErrors.byNewsShowcaseNotAllowedWithPage());
1773
1928
  const saFilters = apiLeafFilters.filter((f) => f.dimension === "searchAppearance");
1774
1929
  const hasNewsShowcase = saFilters.some((f) => f.operator === "equals" && f.expression === "NEWS_SHOWCASE");
1775
1930
  const hasOther = saFilters.some((f) => !(f.operator === "equals" && f.expression === "NEWS_SHOWCASE"));
1776
- if (!hasNewsShowcase || hasOther) throw new Error("aggregationType: \"byNewsShowcasePanel\" requires a searchAppearance equals \"NEWS_SHOWCASE\" filter and no other searchAppearance filter");
1931
+ if (!hasNewsShowcase || hasOther) return err(queryErrors.byNewsShowcaseRequiresShowcaseFilter());
1777
1932
  }
1778
1933
  body.aggregationType = state.aggregationType;
1779
1934
  }
1780
- return body;
1935
+ return ok(body);
1936
+ }
1937
+ function resolveToBody(state) {
1938
+ return unwrapResult(resolveToBodyResult(state), queryErrorToException);
1781
1939
  }
1782
1940
  function isApiFilter(f) {
1783
1941
  return !isMetricOperator(f.operator) && !isSpecialOperator(f.operator);
@@ -2031,12 +2189,6 @@ function between(column, start, end) {
2031
2189
  function topLevel(column) {
2032
2190
  return leafFilter(column.dimension, "topLevel", "");
2033
2191
  }
2034
- var UnsupportedLogicalCapabilityError = class extends Error {
2035
- constructor(capability, context) {
2036
- super(`${context} requires ${capability} capability`);
2037
- this.name = "UnsupportedLogicalCapabilityError";
2038
- }
2039
- };
2040
2192
  function collectInternalFilters(filter) {
2041
2193
  if (!filter || !("_filters" in filter)) return [];
2042
2194
  const flat = filter._filters;
@@ -2079,17 +2231,6 @@ function isDatasetResolvable(dimensions, filterDims = []) {
2079
2231
  if (needed.size === 0) return true;
2080
2232
  return RESOLVABLE_DIMENSION_FAMILIES.some((family) => [...needed].every((d) => family.has(d)));
2081
2233
  }
2082
- var UnresolvableDatasetError = class extends Error {
2083
- constructor(dimensions, filterDims = []) {
2084
- const grouped = dimensions.filter((d) => !TIME_AXIS_DIMENSIONS.has(d));
2085
- const filtered = filterDims.filter((d) => !TIME_AXIS_DIMENSIONS.has(d));
2086
- super(`Cannot resolve a [${grouped.join(", ")}] breakdown filtered by [${filtered.join(", ")}] from stored data: these dimensions live in separate per-dimension tables. Only the live GSC API computes cross-dimension aggregates.`);
2087
- this.name = "UnresolvableDatasetError";
2088
- }
2089
- };
2090
- function requireCapability(capabilities, capability, enabled, context) {
2091
- if (enabled && !capabilities?.[capability]) throw new UnsupportedLogicalCapabilityError(capability, context);
2092
- }
2093
2234
  function isDimensionLeaf(filter) {
2094
2235
  if (isMetric(filter.dimension)) return false;
2095
2236
  if (filter.dimension === "date" && isDateOperator(filter.operator)) return false;
@@ -2105,20 +2246,19 @@ function toLogicalDimensionFilter(filter) {
2105
2246
  expression2: filter.expression2
2106
2247
  };
2107
2248
  }
2108
- function buildDimensionFilterTree(filter, capabilities) {
2249
+ function buildDimensionFilterTree(filter) {
2109
2250
  if (!filter || !("_filters" in filter)) return void 0;
2110
2251
  const groupType = filter._groupType ?? "and";
2111
2252
  const children = [];
2112
2253
  for (const f of filter._filters) {
2113
2254
  if (!isDimensionLeaf(f)) continue;
2114
- requireCapability(capabilities, "regex", isRegexOperator(f.operator), "logical plan");
2115
2255
  children.push({
2116
2256
  kind: "leaf",
2117
2257
  filter: toLogicalDimensionFilter(f)
2118
2258
  });
2119
2259
  }
2120
2260
  for (const g of filter._nestedGroups ?? []) {
2121
- const sub = buildDimensionFilterTree(g, capabilities);
2261
+ const sub = buildDimensionFilterTree(g);
2122
2262
  if (sub) children.push(sub);
2123
2263
  }
2124
2264
  if (children.length === 0) return void 0;
@@ -2129,11 +2269,12 @@ function buildDimensionFilterTree(filter, capabilities) {
2129
2269
  children
2130
2270
  };
2131
2271
  }
2132
- function buildLogicalPlan(state, capabilities = {}) {
2272
+ function buildLogicalPlanResult(state, capabilities = {}) {
2133
2273
  const normalizedFilter = normalizeFilter(state.filter);
2134
2274
  const { startDate, endDate } = extractDateRange(normalizedFilter);
2135
- if (!startDate || !endDate) throw new Error("logical plan requires date range (use between(date, ...) or gte/lte)");
2275
+ if (!startDate || !endDate) return err(queryErrors.missingDateRange());
2136
2276
  const allFilters = collectInternalFilters(normalizedFilter);
2277
+ if (!capabilities.regex && allFilters.some((f) => isDimensionLeaf(f) && isRegexOperator(f.operator))) return err(queryErrors.unsupportedCapability("regex", "logical plan"));
2137
2278
  const metricFilters = extractMetricFilters(normalizedFilter);
2138
2279
  const specialFilters = extractSpecialOperatorFilters(normalizedFilter);
2139
2280
  const prefilters = extractMetricFilters(normalizeFilter(state.prefilter));
@@ -2147,19 +2288,17 @@ function buildLogicalPlan(state, capabilities = {}) {
2147
2288
  queryParams[filter.dimension] = filter.expression;
2148
2289
  continue;
2149
2290
  }
2150
- const operator = filter.operator;
2151
- requireCapability(capabilities, "regex", isRegexOperator(operator), "logical plan");
2152
2291
  dimensionFilters.push({
2153
2292
  dimension: filter.dimension,
2154
- operator,
2293
+ operator: filter.operator,
2155
2294
  expression: filter.expression,
2156
2295
  expression2: filter.expression2
2157
2296
  });
2158
2297
  }
2159
- const dimensionFilterTree = buildDimensionFilterTree(normalizedFilter, capabilities);
2298
+ const dimensionFilterTree = buildDimensionFilterTree(normalizedFilter);
2160
2299
  const filterDims = dimensionFilters.map((filter) => filter.dimension);
2161
- if (!isDatasetResolvable(state.dimensions, filterDims)) throw new UnresolvableDatasetError(state.dimensions, filterDims);
2162
- return {
2300
+ if (!isDatasetResolvable(state.dimensions, filterDims)) return err(queryErrors.unresolvableDataset(state.dimensions, filterDims));
2301
+ return ok({
2163
2302
  dataset: inferDataset(state.dimensions, filterDims),
2164
2303
  dimensions: [...state.dimensions],
2165
2304
  groupByDimensions: state.dimensions.filter((d) => d !== "date"),
@@ -2193,18 +2332,26 @@ function buildLogicalPlan(state, capabilities = {}) {
2193
2332
  orderBy: state.orderBy,
2194
2333
  rowLimit: state.rowLimit,
2195
2334
  startRow: state.startRow
2196
- };
2335
+ });
2197
2336
  }
2198
- function buildLogicalComparisonPlan(current, previous, capabilities = {}, comparisonFilter) {
2199
- requireCapability(capabilities, "comparisonJoin", true, "logical comparison plan");
2200
- const currentPlan = buildLogicalPlan(current, capabilities);
2201
- const previousPlan = buildLogicalPlan(previous, capabilities);
2202
- requireCapability(capabilities, "multiDataset", currentPlan.dataset !== previousPlan.dataset, "logical comparison plan");
2203
- return {
2204
- current: currentPlan,
2205
- previous: previousPlan,
2337
+ function buildLogicalPlan(state, capabilities = {}) {
2338
+ return unwrapResult(buildLogicalPlanResult(state, capabilities), queryErrorToException);
2339
+ }
2340
+ function buildLogicalComparisonPlanResult(current, previous, capabilities = {}, comparisonFilter) {
2341
+ if (!capabilities.comparisonJoin) return err(queryErrors.unsupportedCapability("comparisonJoin", "logical comparison plan"));
2342
+ const currentResult = buildLogicalPlanResult(current, capabilities);
2343
+ if (!currentResult.ok) return currentResult;
2344
+ const previousResult = buildLogicalPlanResult(previous, capabilities);
2345
+ if (!previousResult.ok) return previousResult;
2346
+ if (currentResult.value.dataset !== previousResult.value.dataset && !capabilities.multiDataset) return err(queryErrors.unsupportedCapability("multiDataset", "logical comparison plan"));
2347
+ return ok({
2348
+ current: currentResult.value,
2349
+ previous: previousResult.value,
2206
2350
  comparisonFilter
2207
- };
2351
+ });
2352
+ }
2353
+ function buildLogicalComparisonPlan(current, previous, capabilities = {}, comparisonFilter) {
2354
+ return unwrapResult(buildLogicalComparisonPlanResult(current, previous, capabilities, comparisonFilter), queryErrorToException);
2208
2355
  }
2209
2356
  function today() {
2210
2357
  return currentPstDate();
@@ -2212,4 +2359,4 @@ function today() {
2212
2359
  function daysAgo(n) {
2213
2360
  return daysAgoPst(n);
2214
2361
  }
2215
- export { Countries, Devices, SearchTypes, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalPlan, clicks, contains, country, ctr, currentPstDate, date, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, gsc, gt, gte, hour, impressions, inArray, isJsonFilter, like, lt, lte, ne, normalizeBuilderState, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, regex, searchAppearance, searchType, today, topLevel };
2362
+ export { Countries, Devices, SearchTypes, UnresolvableDatasetError, UnsupportedLogicalCapabilityError, and, between, buildLogicalComparisonPlan, buildLogicalComparisonPlanResult, buildLogicalPlan, buildLogicalPlanResult, clicks, contains, country, ctr, currentPstDate, date, daysAgo, device, eq, extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, formatQueryError, gsc, gt, gte, hour, impressions, inArray, isJsonFilter, isQueryError, like, lt, lte, ne, normalizeBuilderState, normalizeBuilderStateResult, normalizeFilter, not, notRegex, or, page, parseJsonFilter, position, query, queryCanonical, queryErrorToException, queryErrors, regex, resolveToBody, resolveToBodyResult, searchAppearance, searchType, today, topLevel };
@@ -1,6 +1,15 @@
1
1
  import { TableName } from "@gscdump/contracts";
2
2
  type GscDataState = 'final' | 'all' | 'hourly_all';
3
3
  type GscAggregationType = 'auto' | 'byPage' | 'byProperty' | 'byNewsShowcasePanel';
4
+ interface Ok<A> {
5
+ readonly ok: true;
6
+ readonly value: A;
7
+ }
8
+ interface Err<E> {
9
+ readonly ok: false;
10
+ readonly error: E;
11
+ }
12
+ type Result<A, E> = Ok<A> | Err<E>;
4
13
  declare const _default: {
5
14
  name: string;
6
15
  'alpha-2': string;
@@ -76,6 +85,62 @@ interface BuilderState {
76
85
  /** GSC search corpus. Wins over any `searchType` filter when both are set. */
77
86
  searchType?: SearchType;
78
87
  }
88
+ type QueryErrorKind = 'missing-date-range' | 'invalid-row-limit' | 'invalid-start-row' | 'invalid-data-state' | 'invalid-aggregation-type' | 'invalid-builder-state' | 'unsupported-capability' | 'unresolvable-dataset';
89
+ type QueryError = {
90
+ kind: 'missing-date-range';
91
+ message: string;
92
+ } | {
93
+ kind: 'invalid-row-limit';
94
+ value: unknown;
95
+ message: string;
96
+ } | {
97
+ kind: 'invalid-start-row';
98
+ value: unknown;
99
+ message: string;
100
+ } | {
101
+ kind: 'invalid-data-state';
102
+ message: string;
103
+ } | {
104
+ kind: 'invalid-aggregation-type';
105
+ message: string;
106
+ } | {
107
+ kind: 'invalid-builder-state';
108
+ message: string;
109
+ cause?: unknown;
110
+ } | {
111
+ kind: 'unsupported-capability';
112
+ capability: string;
113
+ context: string;
114
+ message: string;
115
+ } | {
116
+ kind: 'unresolvable-dataset';
117
+ dimensions: readonly Dimension[];
118
+ filterDims: readonly Dimension[];
119
+ message: string;
120
+ };
121
+ /**
122
+ * Thrown when a query needs a planner capability (regex pushdown, comparison
123
+ * joins, multi-dataset reads) the target engine lacks. Engines catch it to fall
124
+ * back to the live GSC API. Carries the typed `queryError` value so a caller can
125
+ * read the modelled failure instead of parsing the message.
126
+ */
127
+ declare class UnsupportedLogicalCapabilityError extends Error {
128
+ readonly queryError: Extract<QueryError, {
129
+ kind: 'unsupported-capability';
130
+ }>;
131
+ constructor(capability: string, context: string);
132
+ }
133
+ /**
134
+ * Thrown when a query's grouped + filtered dimensions span more than one stored
135
+ * dataset. Replaces the resolver's raw "unknown column" error so hosts can map
136
+ * it to a 4xx instead of leaking an opaque 500. Carries the typed `queryError`.
137
+ */
138
+ declare class UnresolvableDatasetError extends Error {
139
+ readonly queryError: Extract<QueryError, {
140
+ kind: 'unresolvable-dataset';
141
+ }>;
142
+ constructor(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]);
143
+ }
79
144
  type LogicalDataset = TableName;
80
145
  type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
81
146
  interface PlannerCapabilities {
@@ -133,9 +198,6 @@ interface LogicalComparisonPlan {
133
198
  previous: LogicalQueryPlan;
134
199
  comparisonFilter?: ComparisonFilter;
135
200
  }
136
- declare class UnsupportedLogicalCapabilityError extends Error {
137
- constructor(capability: keyof PlannerCapabilities, context: string);
138
- }
139
201
  declare function inferDataset(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]): LogicalDataset;
140
202
  /**
141
203
  * True when every grouped + filtered dimension fits inside one stored dataset,
@@ -145,14 +207,6 @@ declare function inferDataset(dimensions: readonly Dimension[], filterDims?: rea
145
207
  * time.
146
208
  */
147
209
  declare function isDatasetResolvable(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]): boolean;
148
- /**
149
- * Thrown when a query's grouped + filtered dimensions span more than one
150
- * stored dataset. Replaces the resolver's raw "unknown column" error so hosts
151
- * can map it to a 4xx instead of leaking an opaque 500.
152
- */
153
- declare class UnresolvableDatasetError extends Error {
154
- constructor(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]);
155
- }
156
210
  /**
157
211
  * `BuilderState`-level convenience for {@link isDatasetResolvable}: extracts
158
212
  * the state's dimension filters (the same way `buildLogicalPlan` does) and
@@ -160,6 +214,19 @@ declare class UnresolvableDatasetError extends Error {
160
214
  * composite source) detect a cross-dimension query without rebuilding a plan.
161
215
  */
162
216
  declare function isStateResolvable(state: BuilderState): boolean;
217
+ /**
218
+ * Errors-as-values core: builds the logical plan or returns a typed `QueryError`
219
+ * for every modelled failure (missing date range, a regex filter on an engine
220
+ * without regex pushdown, a cross-dimension query with no stored home).
221
+ * `buildLogicalPlan` is the throwing wrapper for call sites that prefer exceptions.
222
+ */
223
+ declare function buildLogicalPlanResult(state: BuilderState, capabilities?: PlannerCapabilities): Result<LogicalQueryPlan, QueryError>;
163
224
  declare function buildLogicalPlan(state: BuilderState, capabilities?: PlannerCapabilities): LogicalQueryPlan;
225
+ /**
226
+ * Errors-as-values core for the comparison plan: returns a typed `QueryError`
227
+ * when the engine lacks the comparison-join or multi-dataset capability the
228
+ * paired queries need, or when either side fails to plan.
229
+ */
230
+ declare function buildLogicalComparisonPlanResult(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): Result<LogicalComparisonPlan, QueryError>;
164
231
  declare function buildLogicalComparisonPlan(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): LogicalComparisonPlan;
165
- export { ComparisonFilter, LogicalComparisonPlan, LogicalDataset, LogicalDimensionFilter, LogicalFilterGroup, LogicalFilterLeaf, LogicalFilterNode, LogicalMetricFilter, LogicalQueryPlan, PlannerCapabilities, type TableName, UnresolvableDatasetError, UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalPlan, inferDataset, isDatasetResolvable, isStateResolvable };
232
+ export { ComparisonFilter, LogicalComparisonPlan, LogicalDataset, LogicalDimensionFilter, LogicalFilterGroup, LogicalFilterLeaf, LogicalFilterNode, LogicalMetricFilter, LogicalQueryPlan, PlannerCapabilities, type QueryError, type QueryErrorKind, type TableName, UnresolvableDatasetError, UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalComparisonPlanResult, buildLogicalPlan, buildLogicalPlanResult, inferDataset, isDatasetResolvable, isStateResolvable };