gscdump 0.19.1 → 0.19.3

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.
@@ -2042,6 +2042,30 @@ function inferDataset(dimensions, filterDims = []) {
2042
2042
  if (has("device")) return "devices";
2043
2043
  return "devices";
2044
2044
  }
2045
+ const RESOLVABLE_DIMENSION_FAMILIES = [
2046
+ new Set([
2047
+ "page",
2048
+ "query",
2049
+ "queryCanonical"
2050
+ ]),
2051
+ new Set(["country"]),
2052
+ new Set(["device"]),
2053
+ new Set(["searchAppearance"])
2054
+ ];
2055
+ const TIME_AXIS_DIMENSIONS = new Set(["date", "hour"]);
2056
+ function isDatasetResolvable(dimensions, filterDims = []) {
2057
+ const needed = new Set([...dimensions, ...filterDims].filter((d) => !TIME_AXIS_DIMENSIONS.has(d)));
2058
+ if (needed.size === 0) return true;
2059
+ return RESOLVABLE_DIMENSION_FAMILIES.some((family) => [...needed].every((d) => family.has(d)));
2060
+ }
2061
+ var UnresolvableDatasetError = class extends Error {
2062
+ constructor(dimensions, filterDims = []) {
2063
+ const grouped = dimensions.filter((d) => !TIME_AXIS_DIMENSIONS.has(d));
2064
+ const filtered = filterDims.filter((d) => !TIME_AXIS_DIMENSIONS.has(d));
2065
+ 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.`);
2066
+ this.name = "UnresolvableDatasetError";
2067
+ }
2068
+ };
2045
2069
  function requireCapability(capabilities, capability, enabled, context) {
2046
2070
  if (enabled && !capabilities?.[capability]) throw new UnsupportedLogicalCapabilityError(capability, context);
2047
2071
  }
@@ -2113,6 +2137,7 @@ function buildLogicalPlan(state, capabilities = {}) {
2113
2137
  }
2114
2138
  const dimensionFilterTree = buildDimensionFilterTree(normalizedFilter, capabilities);
2115
2139
  const filterDims = dimensionFilters.map((filter) => filter.dimension);
2140
+ if (!isDatasetResolvable(state.dimensions, filterDims)) throw new UnresolvableDatasetError(state.dimensions, filterDims);
2116
2141
  return {
2117
2142
  dataset: inferDataset(state.dimensions, filterDims),
2118
2143
  dimensions: [...state.dimensions],
@@ -136,6 +136,30 @@ interface LogicalComparisonPlan {
136
136
  declare class UnsupportedLogicalCapabilityError extends Error {
137
137
  constructor(capability: keyof PlannerCapabilities, context: string);
138
138
  }
139
+ declare function inferDataset(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]): LogicalDataset;
140
+ /**
141
+ * True when every grouped + filtered dimension fits inside one stored dataset,
142
+ * i.e. the query is answerable from stored Parquet/D1 tables without a live
143
+ * GSC call. `inferDataset` always returns *some* dataset; this predicate is
144
+ * how callers tell a genuine match from one that will fail at column-resolve
145
+ * time.
146
+ */
147
+ 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
+ /**
157
+ * `BuilderState`-level convenience for {@link isDatasetResolvable}: extracts
158
+ * the state's dimension filters (the same way `buildLogicalPlan` does) and
159
+ * checks them against the grouped dimensions. Lets routing code (e.g. the
160
+ * composite source) detect a cross-dimension query without rebuilding a plan.
161
+ */
162
+ declare function isStateResolvable(state: BuilderState): boolean;
139
163
  declare function buildLogicalPlan(state: BuilderState, capabilities?: PlannerCapabilities): LogicalQueryPlan;
140
164
  declare function buildLogicalComparisonPlan(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): LogicalComparisonPlan;
141
- export { ComparisonFilter, LogicalComparisonPlan, LogicalDataset, LogicalDimensionFilter, LogicalFilterGroup, LogicalFilterLeaf, LogicalFilterNode, LogicalMetricFilter, LogicalQueryPlan, PlannerCapabilities, type TableName, UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalPlan };
165
+ export { ComparisonFilter, LogicalComparisonPlan, LogicalDataset, LogicalDimensionFilter, LogicalFilterGroup, LogicalFilterLeaf, LogicalFilterNode, LogicalMetricFilter, LogicalQueryPlan, PlannerCapabilities, type TableName, UnresolvableDatasetError, UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalPlan, inferDataset, isDatasetResolvable, isStateResolvable };
@@ -1688,6 +1688,34 @@ function inferDataset(dimensions, filterDims = []) {
1688
1688
  if (has("device")) return "devices";
1689
1689
  return "devices";
1690
1690
  }
1691
+ const RESOLVABLE_DIMENSION_FAMILIES = [
1692
+ new Set([
1693
+ "page",
1694
+ "query",
1695
+ "queryCanonical"
1696
+ ]),
1697
+ new Set(["country"]),
1698
+ new Set(["device"]),
1699
+ new Set(["searchAppearance"])
1700
+ ];
1701
+ const TIME_AXIS_DIMENSIONS = new Set(["date", "hour"]);
1702
+ function isDatasetResolvable(dimensions, filterDims = []) {
1703
+ const needed = new Set([...dimensions, ...filterDims].filter((d) => !TIME_AXIS_DIMENSIONS.has(d)));
1704
+ if (needed.size === 0) return true;
1705
+ return RESOLVABLE_DIMENSION_FAMILIES.some((family) => [...needed].every((d) => family.has(d)));
1706
+ }
1707
+ var UnresolvableDatasetError = class extends Error {
1708
+ constructor(dimensions, filterDims = []) {
1709
+ const grouped = dimensions.filter((d) => !TIME_AXIS_DIMENSIONS.has(d));
1710
+ const filtered = filterDims.filter((d) => !TIME_AXIS_DIMENSIONS.has(d));
1711
+ 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.`);
1712
+ this.name = "UnresolvableDatasetError";
1713
+ }
1714
+ };
1715
+ function isStateResolvable(state) {
1716
+ const filterDims = collectInternalFilters(normalizeFilter(state.filter)).filter((f) => !isMetric(f.dimension)).filter((f) => !(f.dimension === "date" && isDateOperator(f.operator))).filter((f) => !(f.operator === "topLevel" || f.operator.startsWith("metric"))).filter((f) => !isQueryParam(f.dimension)).map((f) => f.dimension);
1717
+ return isDatasetResolvable(state.dimensions, filterDims);
1718
+ }
1691
1719
  function requireCapability(capabilities, capability, enabled, context) {
1692
1720
  if (enabled && !capabilities?.[capability]) throw new UnsupportedLogicalCapabilityError(capability, context);
1693
1721
  }
@@ -1759,6 +1787,7 @@ function buildLogicalPlan(state, capabilities = {}) {
1759
1787
  }
1760
1788
  const dimensionFilterTree = buildDimensionFilterTree(normalizedFilter, capabilities);
1761
1789
  const filterDims = dimensionFilters.map((filter) => filter.dimension);
1790
+ if (!isDatasetResolvable(state.dimensions, filterDims)) throw new UnresolvableDatasetError(state.dimensions, filterDims);
1762
1791
  return {
1763
1792
  dataset: inferDataset(state.dimensions, filterDims),
1764
1793
  dimensions: [...state.dimensions],
@@ -1806,4 +1835,4 @@ function buildLogicalComparisonPlan(current, previous, capabilities = {}, compar
1806
1835
  comparisonFilter
1807
1836
  };
1808
1837
  }
1809
- export { UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalPlan };
1838
+ export { UnresolvableDatasetError, UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalPlan, inferDataset, isDatasetResolvable, isStateResolvable };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gscdump",
3
3
  "type": "module",
4
- "version": "0.19.1",
4
+ "version": "0.19.3",
5
5
  "description": "Google Search Console API wrapper with typed query builder, streaming pagination, and SEO analysis functions",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -108,7 +108,7 @@
108
108
  "defu": "^6.1.7",
109
109
  "ofetch": "^1.5.1",
110
110
  "ufo": "^1.6.4",
111
- "@gscdump/contracts": "0.19.1"
111
+ "@gscdump/contracts": "0.19.3"
112
112
  },
113
113
  "devDependencies": {
114
114
  "@googleapis/indexing": "^6.0.1",