gscdump 3.5.0 → 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.
Files changed (48) hide show
  1. package/README.md +36 -7
  2. package/dist/api/batch.d.mts +2 -3
  3. package/dist/api/batch.mjs +2 -0
  4. package/dist/api/indexing.d.mts +7 -8
  5. package/dist/api/indexing.mjs +1 -1
  6. package/dist/api/inspection.d.mts +21 -22
  7. package/dist/api/oauth.d.mts +8 -9
  8. package/dist/api/oauth.mjs +9 -1
  9. package/dist/api/sites.d.mts +9 -10
  10. package/dist/api/verification.d.mts +8 -9
  11. package/dist/bing/client.d.mts +3 -4
  12. package/dist/bing/client.mjs +10 -4
  13. package/dist/bing/normalize.d.mts +9 -10
  14. package/dist/bing/types.d.mts +42 -38
  15. package/dist/contracts.d.mts +12 -12
  16. package/dist/core/canonical.d.mts +3 -4
  17. package/dist/core/client.d.mts +16 -17
  18. package/dist/core/client.mjs +16 -13
  19. package/dist/core/errors.d.mts +9 -10
  20. package/dist/core/errors.mjs +2 -1
  21. package/dist/core/gsc-dates.d.mts +22 -23
  22. package/dist/core/indexing-issues.d.mts +10 -11
  23. package/dist/core/property.d.mts +9 -10
  24. package/dist/core/quota.d.mts +1 -2
  25. package/dist/core/result.d.mts +8 -9
  26. package/dist/core/scope-values.d.mts +5 -6
  27. package/dist/core/scopes.d.mts +3 -4
  28. package/dist/core/site-url.d.mts +4 -5
  29. package/dist/core/types.d.mts +29 -30
  30. package/dist/core/window.d.mts +5 -6
  31. package/dist/normalize.d.mts +1 -2
  32. package/dist/query/builder.d.mts +4 -5
  33. package/dist/query/columns.d.mts +13 -14
  34. package/dist/query/constants.d.mts +5 -5
  35. package/dist/query/errors.d.mts +7 -8
  36. package/dist/query/errors.mjs +1 -1
  37. package/dist/query/index.d.mts +3 -3
  38. package/dist/query/operators.d.mts +22 -23
  39. package/dist/query/plan.d.mts +18 -18
  40. package/dist/query/plan.mjs +7 -4
  41. package/dist/query/resolver.d.mts +10 -19
  42. package/dist/query/resolver.mjs +125 -61
  43. package/dist/query/types.d.mts +22 -23
  44. package/dist/query/utils/dayjs.d.mts +2 -3
  45. package/dist/sitemap-identity.d.mts +8 -9
  46. package/dist/tenant.d.mts +3 -4
  47. package/dist/url.d.mts +1 -2
  48. package/package.json +2 -2
@@ -2,37 +2,37 @@ import { Result } from "../core/result.mjs";
2
2
  import { TableName } from "../contracts.mjs";
3
3
  import { BuilderState, Dimension, FilterOperator, Metric, MetricOperator, QueryParamName } from "./types.mjs";
4
4
  import { QueryError, QueryErrorKind, UnresolvableDatasetError, UnsupportedLogicalCapabilityError } from "./errors.mjs";
5
- type LogicalDataset = TableName;
6
- type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
7
- interface PlannerCapabilities {
5
+ export type LogicalDataset = TableName;
6
+ export type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
7
+ export interface PlannerCapabilities {
8
8
  regex?: boolean;
9
9
  multiDataset?: boolean;
10
10
  comparisonJoin?: boolean;
11
11
  windowTotals?: boolean;
12
12
  }
13
- interface LogicalDimensionFilter {
13
+ export interface LogicalDimensionFilter {
14
14
  dimension: Dimension;
15
15
  operator: FilterOperator;
16
16
  expression: string;
17
17
  expression2?: string;
18
18
  }
19
- interface LogicalMetricFilter {
19
+ export interface LogicalMetricFilter {
20
20
  metric: Metric;
21
21
  operator: MetricOperator;
22
22
  expression: number;
23
23
  expression2?: number;
24
24
  }
25
- interface LogicalFilterLeaf {
25
+ export interface LogicalFilterLeaf {
26
26
  kind: 'leaf';
27
27
  filter: LogicalDimensionFilter;
28
28
  }
29
- interface LogicalFilterGroup {
29
+ export interface LogicalFilterGroup {
30
30
  kind: 'group';
31
31
  groupType: 'and' | 'or';
32
32
  children: LogicalFilterNode[];
33
33
  }
34
- type LogicalFilterNode = LogicalFilterLeaf | LogicalFilterGroup;
35
- interface LogicalQueryPlan {
34
+ export type LogicalFilterNode = LogicalFilterLeaf | LogicalFilterGroup;
35
+ export interface LogicalQueryPlan {
36
36
  dataset: LogicalDataset;
37
37
  dimensions: Dimension[];
38
38
  groupByDimensions: Dimension[];
@@ -54,12 +54,12 @@ interface LogicalQueryPlan {
54
54
  rowLimit?: number;
55
55
  startRow?: number;
56
56
  }
57
- interface LogicalComparisonPlan {
57
+ export interface LogicalComparisonPlan {
58
58
  current: LogicalQueryPlan;
59
59
  previous: LogicalQueryPlan;
60
60
  comparisonFilter?: ComparisonFilter;
61
61
  }
62
- declare function inferDataset(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]): LogicalDataset;
62
+ export declare function inferDataset(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]): LogicalDataset;
63
63
  /**
64
64
  * True when every grouped + filtered dimension fits inside one stored dataset,
65
65
  * i.e. the query is answerable from stored Parquet/D1 tables without a live
@@ -67,27 +67,27 @@ declare function inferDataset(dimensions: readonly Dimension[], filterDims?: rea
67
67
  * how callers tell a genuine match from one that will fail at column-resolve
68
68
  * time.
69
69
  */
70
- declare function isDatasetResolvable(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]): boolean;
70
+ export declare function isDatasetResolvable(dimensions: readonly Dimension[], filterDims?: readonly Dimension[]): boolean;
71
71
  /**
72
72
  * `BuilderState`-level convenience for {@link isDatasetResolvable}: extracts
73
73
  * the state's dimension filters (the same way `buildLogicalPlan` does) and
74
74
  * checks them against the grouped dimensions. Lets routing code (e.g. the
75
75
  * composite source) detect a cross-dimension query without rebuilding a plan.
76
76
  */
77
- declare function isStateResolvable(state: BuilderState): boolean;
77
+ export declare function isStateResolvable(state: BuilderState): boolean;
78
78
  /**
79
79
  * Errors-as-values core: builds the logical plan or returns a typed `QueryError`
80
80
  * for every modelled failure (missing date range, a regex filter on an engine
81
81
  * without regex pushdown, a cross-dimension query with no stored home).
82
82
  * `buildLogicalPlan` is the throwing wrapper for call sites that prefer exceptions.
83
83
  */
84
- declare function buildLogicalPlanResult(state: BuilderState, capabilities?: PlannerCapabilities): Result<LogicalQueryPlan, QueryError>;
85
- declare function buildLogicalPlan(state: BuilderState, capabilities?: PlannerCapabilities): LogicalQueryPlan;
84
+ export declare function buildLogicalPlanResult(input: BuilderState, capabilities?: PlannerCapabilities): Result<LogicalQueryPlan, QueryError>;
85
+ export declare function buildLogicalPlan(state: BuilderState, capabilities?: PlannerCapabilities): LogicalQueryPlan;
86
86
  /**
87
87
  * Errors-as-values core for the comparison plan: returns a typed `QueryError`
88
88
  * when the engine lacks the comparison-join or multi-dataset capability the
89
89
  * paired queries need, or when either side fails to plan.
90
90
  */
91
- declare function buildLogicalComparisonPlanResult(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): Result<LogicalComparisonPlan, QueryError>;
92
- declare function buildLogicalComparisonPlan(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): LogicalComparisonPlan;
93
- 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 };
91
+ export declare function buildLogicalComparisonPlanResult(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): Result<LogicalComparisonPlan, QueryError>;
92
+ export declare function buildLogicalComparisonPlan(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): LogicalComparisonPlan;
93
+ export { type QueryError, type QueryErrorKind, type TableName, UnresolvableDatasetError, UnsupportedLogicalCapabilityError };
@@ -1,7 +1,7 @@
1
1
  import { err, ok, unwrapResult } from "../core/result.mjs";
2
2
  import { UnresolvableDatasetError, UnsupportedLogicalCapabilityError, queryErrorToException, queryErrors } from "./errors.mjs";
3
3
  import { isDateOperator, isMetric, isQueryParam, isRegexOperator } from "./operator-meta.mjs";
4
- import { extractDateRange, extractMetricFilters, extractSpecialOperatorFilters, normalizeFilter } from "./resolver.mjs";
4
+ import { extractDateRange, extractMetricFilters, extractSpecialOperatorFilters, normalizeBuilderStateResult, normalizeFilter } from "./resolver.mjs";
5
5
  function collectInternalFilters(filter) {
6
6
  if (!filter || !("_filters" in filter)) return [];
7
7
  const flat = filter._filters;
@@ -86,15 +86,18 @@ function buildDimensionFilterTree(filter) {
86
86
  children
87
87
  };
88
88
  }
89
- function buildLogicalPlanResult(state, capabilities = {}) {
90
- const normalizedFilter = normalizeFilter(state.filter);
89
+ function buildLogicalPlanResult(input, capabilities = {}) {
90
+ const parsed = normalizeBuilderStateResult(input);
91
+ if (!parsed.ok) return parsed;
92
+ const state = parsed.value;
93
+ const normalizedFilter = state.filter;
91
94
  const { startDate, endDate } = extractDateRange(normalizedFilter);
92
95
  if (!startDate || !endDate) return err(queryErrors.missingDateRange());
93
96
  const allFilters = collectInternalFilters(normalizedFilter);
94
97
  if (!capabilities.regex && allFilters.some((f) => isDimensionLeaf(f) && isRegexOperator(f.operator))) return err(queryErrors.unsupportedCapability("regex", "logical plan"));
95
98
  const metricFilters = extractMetricFilters(normalizedFilter);
96
99
  const specialFilters = extractSpecialOperatorFilters(normalizedFilter);
97
- const normalizedPrefilter = normalizeFilter(state.prefilter);
100
+ const normalizedPrefilter = state.prefilter;
98
101
  const prefilters = extractMetricFilters(normalizedPrefilter);
99
102
  const queryParams = {};
100
103
  const dimensionFilters = [];
@@ -3,31 +3,23 @@ import { GscSearchAnalyticsRequest } from "../contracts.mjs";
3
3
  import { SearchType } from "./constants.mjs";
4
4
  import { BuilderState, Filter, FilterInput, InternalFilter } from "./types.mjs";
5
5
  import { QueryError } from "./errors.mjs";
6
- declare function normalizeFilter(input?: FilterInput): Filter<any> | undefined;
7
- /**
8
- * Errors-as-values core for {@link normalizeBuilderState}: returns an
9
- * `invalid-builder-state` `QueryError` when the untrusted partner-API body is
10
- * not an object, and an `invalid-filter` `QueryError` when a filter leaf lacks
11
- * its string `dimension`/`operator`, instead of throwing. Also coerces
12
- * alternative `orderBy` shapes into the canonical `{ column, dir }`.
13
- * Receive-edge parse (parse, don't validate), so hosts can map a bad body to a
14
- * 4xx and downstream consumers only ever see the canonical shape.
15
- */
16
- declare function normalizeBuilderStateResult(state: unknown): Result<BuilderState, QueryError>;
17
- declare function normalizeBuilderState(state: unknown): BuilderState;
18
- declare function extractDateRange(input?: FilterInput): {
6
+ export declare function normalizeFilter(input?: FilterInput): Filter<any> | undefined;
7
+ /** Parse an untrusted query body. Expected input failures stay in the error channel. */
8
+ export declare function normalizeBuilderStateResult(state: unknown): Result<BuilderState, QueryError>;
9
+ export declare function normalizeBuilderState(state: unknown): BuilderState;
10
+ export declare function extractDateRange(input?: FilterInput): {
19
11
  startDate?: string;
20
12
  endDate?: string;
21
13
  };
22
- declare function extractMetricFilters(input?: FilterInput): InternalFilter[];
23
- declare function extractSpecialOperatorFilters(input?: FilterInput): InternalFilter[];
14
+ export declare function extractMetricFilters(input?: FilterInput): InternalFilter[];
15
+ export declare function extractSpecialOperatorFilters(input?: FilterInput): InternalFilter[];
24
16
  /**
25
17
  * Pull `searchType` out of a BuilderState filter. Returns undefined for
26
18
  * missing/invalid shapes — callers treat that as "no scope" (cross-type read).
27
19
  * Validated against the canonical `SearchTypes` set so unknown strings
28
20
  * don't reach the engine.
29
21
  */
30
- declare function extractSearchType(state: BuilderState | undefined | null): SearchType | undefined;
22
+ export declare function extractSearchType(state: BuilderState | undefined | null): SearchType | undefined;
31
23
  /**
32
24
  * Errors-as-values core: turns a `BuilderState` into a GSC API request body or
33
25
  * returns a typed `QueryError` for every modelled bad-query case (missing date
@@ -35,6 +27,5 @@ declare function extractSearchType(state: BuilderState | undefined | null): Sear
35
27
  * aggregationType combination). `resolveToBody` is the throwing wrapper over this
36
28
  * for `.toBody()` and the live-API client paths.
37
29
  */
38
- declare function resolveToBodyResult(state: BuilderState): Result<GscSearchAnalyticsRequest, QueryError>;
39
- declare function resolveToBody(state: BuilderState): GscSearchAnalyticsRequest;
40
- export { extractDateRange, extractMetricFilters, extractSearchType, extractSpecialOperatorFilters, normalizeBuilderState, normalizeBuilderStateResult, normalizeFilter, resolveToBody, resolveToBodyResult };
30
+ export declare function resolveToBodyResult(input: BuilderState): Result<GscSearchAnalyticsRequest, QueryError>;
31
+ export declare function resolveToBody(state: BuilderState): GscSearchAnalyticsRequest;
@@ -2,91 +2,152 @@ import { err, ok, unwrapResult } from "../core/result.mjs";
2
2
  import { addDays } from "../core/gsc-dates.mjs";
3
3
  import { SearchTypes } from "./constants.mjs";
4
4
  import { queryErrorToException, queryErrors } from "./errors.mjs";
5
- import { isDateOperator, isMetricOperator, isQueryParam, isSpecialOperator } from "./operator-meta.mjs";
5
+ import { isDateOperator, isMetric, isMetricOperator, isQueryParam, isSpecialOperator } from "./operator-meta.mjs";
6
+ const KNOWN_DIMENSIONS = /* @__PURE__ */ new Set([
7
+ "page",
8
+ "query",
9
+ "queryCanonical",
10
+ "country",
11
+ "device",
12
+ "date",
13
+ "hour",
14
+ "searchAppearance"
15
+ ]);
16
+ const FILTER_OPERATORS = /* @__PURE__ */ new Set([
17
+ "equals",
18
+ "notEquals",
19
+ "contains",
20
+ "notContains",
21
+ "includingRegex",
22
+ "excludingRegex"
23
+ ]);
6
24
  const KNOWN_SEARCH_TYPES = new Set(Object.values(SearchTypes));
7
- function isWireGroupType(type) {
8
- return type === "and" || type === "or";
9
- }
10
- function convertWireLeaf(alt) {
11
- if (!alt.column || !alt.type || isWireGroupType(alt.type)) return null;
12
- const f = {
13
- dimension: alt.column,
14
- operator: alt.type,
15
- expression: alt.type === "between" ? alt.from ?? "" : alt.value ?? ""
16
- };
17
- if (alt.type === "between" && alt.to) f.expression2 = alt.to;
18
- return f;
25
+ function isRecord(value) {
26
+ return value !== null && typeof value === "object" && !Array.isArray(value);
19
27
  }
20
- function convertWireGroup(alt) {
21
- if (!isWireGroupType(alt.type)) {
22
- const leaf = convertWireLeaf(alt);
23
- return leaf ? { _filters: [leaf] } : null;
24
- }
28
+ function isStringArray(value) {
29
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
30
+ }
31
+ function isCalendarDate(value) {
32
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
33
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
34
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
35
+ }
36
+ function parseFilterResult(input, ancestors = /* @__PURE__ */ new Set()) {
37
+ if (input === void 0) return ok(void 0);
38
+ if (!isRecord(input) || ancestors.has(input)) return err(queryErrors.malformedFilterLeaf());
39
+ ancestors.add(input);
40
+ const invalid = () => err(queryErrors.malformedFilterLeaf());
25
41
  const leaves = [];
26
42
  const nested = [];
27
- for (const child of alt.filters ?? []) if (isWireGroupType(child.type)) {
28
- const sub = convertWireGroup(child);
29
- if (sub) nested.push(sub);
30
- } else {
31
- const leaf = convertWireLeaf(child);
32
- if (leaf) leaves.push(leaf);
43
+ const wire = !("_filters" in input);
44
+ const groupType = wire ? input.type : input._groupType;
45
+ const isGroup = groupType === "and" || groupType === "or";
46
+ if (!wire && groupType !== void 0 && !isGroup) return invalid();
47
+ const values = wire ? isGroup ? input.filters : [input] : input._filters;
48
+ if (!Array.isArray(values)) return invalid();
49
+ for (const value of values) {
50
+ if (!isRecord(value)) return invalid();
51
+ if (wire && (value.type === "and" || value.type === "or")) {
52
+ const parsed = parseFilterResult(value, ancestors);
53
+ if (!parsed.ok) return parsed;
54
+ if (parsed.value) nested.push(parsed.value);
55
+ continue;
56
+ }
57
+ const dimension = wire ? value.column : value.dimension;
58
+ const operator = wire ? value.type === "eq" ? "equals" : value.type === "ne" ? "notEquals" : value.type : value.operator;
59
+ const between = operator === "between" || operator === "metricBetween";
60
+ const expression = wire ? between ? value.from : value.value : value.expression;
61
+ const expression2 = wire ? between ? value.to : void 0 : value.expression2;
62
+ if (typeof dimension !== "string" || !dimension || typeof operator !== "string" || !operator || typeof expression !== "string" || expression2 !== void 0 && typeof expression2 !== "string" || between && expression2 === void 0) return invalid();
63
+ if (!KNOWN_DIMENSIONS.has(dimension) && !isMetric(dimension) && !isQueryParam(dimension)) return invalid();
64
+ if (!FILTER_OPERATORS.has(operator) && !isDateOperator(operator) && !isMetricOperator(operator) && !isSpecialOperator(operator)) return invalid();
65
+ if (dimension === "searchType" && (operator !== "equals" || !KNOWN_SEARCH_TYPES.has(expression))) return invalid();
66
+ if (isDateOperator(operator) && (dimension !== "date" || !isCalendarDate(expression) || expression2 !== void 0 && !isCalendarDate(expression2))) return invalid();
67
+ if (isMetricOperator(operator) && (!isMetric(dimension) || !expression.trim() || !Number.isFinite(Number(expression)) || expression2 !== void 0 && (!expression2.trim() || !Number.isFinite(Number(expression2))))) return invalid();
68
+ leaves.push({
69
+ dimension,
70
+ operator,
71
+ expression,
72
+ ...expression2 !== void 0 ? { expression2 } : {}
73
+ });
33
74
  }
34
- if (leaves.length === 0 && nested.length === 0) return null;
35
- return {
75
+ if (!wire && input._nestedGroups !== void 0) {
76
+ if (!Array.isArray(input._nestedGroups)) return invalid();
77
+ for (const group of input._nestedGroups) {
78
+ const parsed = parseFilterResult(group, ancestors);
79
+ if (!parsed.ok) return parsed;
80
+ if (!parsed.value) return invalid();
81
+ nested.push(parsed.value);
82
+ }
83
+ }
84
+ if (groupType === "or" && (leaves.length === 0 || nested.length > 0 || leaves.some((leaf) => leaf.dimension === "searchType" || leaf.dimension === "date" && isDateOperator(leaf.operator)))) return invalid();
85
+ ancestors.delete(input);
86
+ return ok({
36
87
  _filters: leaves,
37
- _nestedGroups: nested.length > 0 ? nested : void 0,
38
- _groupType: alt.type
39
- };
40
- }
41
- function isWireFilter(input) {
42
- if (!input || typeof input !== "object") return false;
43
- const o = input;
44
- if ("_filters" in o) return false;
45
- return "type" in o && typeof o.type === "string" || "filters" in o && Array.isArray(o.filters);
88
+ ...nested.length ? { _nestedGroups: nested } : {},
89
+ ...isGroup ? { _groupType: groupType } : {}
90
+ });
46
91
  }
47
92
  function normalizeFilter(input) {
48
- if (!input) return void 0;
49
- if (isWireFilter(input)) return convertWireGroup(input) ?? void 0;
50
- if (typeof input === "object" && Array.isArray(input._filters)) return input;
93
+ return unwrapResult(parseFilterResult(input), queryErrorToException);
51
94
  }
52
95
  function normalizeOrderBy(orderBy) {
96
+ if (Array.isArray(orderBy) && orderBy.length !== 1) return void 0;
53
97
  const spec = Array.isArray(orderBy) ? orderBy[0] : orderBy;
54
98
  if (!spec || typeof spec !== "object") return void 0;
55
99
  const o = spec;
56
- if (typeof o.column !== "string" || o.column.length === 0) return void 0;
100
+ if (typeof o.column !== "string" || !isMetric(o.column) && o.column !== "date") return void 0;
101
+ if (o.dir !== void 0 && (typeof o.dir !== "string" || !["asc", "desc"].includes(o.dir.toLowerCase()))) return void 0;
102
+ if (o.desc !== void 0 && typeof o.desc !== "boolean") return void 0;
57
103
  const dir = typeof o.dir === "string" ? o.dir.toLowerCase() === "asc" ? "asc" : "desc" : o.desc === false ? "asc" : "desc";
104
+ if (typeof o.desc === "boolean" && o.desc !== (dir === "desc")) return void 0;
58
105
  return {
59
106
  column: o.column,
60
107
  dir
61
108
  };
62
109
  }
63
- function hasMalformedFilterLeaf(filter) {
64
- if (!filter || typeof filter !== "object") return false;
65
- if (Array.isArray(filter._filters)) {
66
- for (const leaf of filter._filters) if (!leaf || typeof leaf !== "object" || typeof leaf.operator !== "string" || typeof leaf.dimension !== "string") return true;
67
- }
68
- if (Array.isArray(filter._nestedGroups)) {
69
- for (const group of filter._nestedGroups) if (hasMalformedFilterLeaf(group)) return true;
70
- }
71
- return false;
72
- }
73
110
  function normalizeBuilderStateResult(state) {
74
- if (!state || typeof state !== "object") return err(queryErrors.invalidBuilderState(state));
111
+ if (!isRecord(state)) return err(queryErrors.invalidBuilderState(state));
75
112
  const s = state;
76
- const filter = normalizeFilter(s.filter);
77
- if (hasMalformedFilterLeaf(filter)) return err(queryErrors.malformedFilterLeaf());
78
- const normalized = {
79
- dimensions: Array.isArray(s.dimensions) ? s.dimensions : [],
113
+ const invalidState = (message) => err({
114
+ ...queryErrors.invalidBuilderState(state),
115
+ message
116
+ });
117
+ if (s.dimensions !== void 0 && (!isStringArray(s.dimensions) || !s.dimensions.every((dimension) => KNOWN_DIMENSIONS.has(dimension)))) return invalidState(`dimensions must contain valid names: ${[...KNOWN_DIMENSIONS].join(", ")}.`);
118
+ if (s.metrics !== void 0 && (!isStringArray(s.metrics) || !s.metrics.every(isMetric))) return invalidState("metrics must contain clicks, impressions, ctr, or position.");
119
+ if (s.searchType !== void 0 && (typeof s.searchType !== "string" || !KNOWN_SEARCH_TYPES.has(s.searchType))) return invalidState(`searchType must be one of: ${[...KNOWN_SEARCH_TYPES].join(", ")}.`);
120
+ if (s.dataState !== void 0 && ![
121
+ "all",
122
+ "final",
123
+ "hourly_all"
124
+ ].includes(s.dataState)) return invalidState("dataState must be all, final, or hourly_all.");
125
+ if (s.aggregationType !== void 0 && ![
126
+ "auto",
127
+ "byPage",
128
+ "byProperty",
129
+ "byNewsShowcasePanel"
130
+ ].includes(s.aggregationType)) return invalidState("aggregationType must be auto, byPage, byProperty, or byNewsShowcasePanel.");
131
+ if (s.rowLimit !== void 0 && (!Number.isSafeInteger(s.rowLimit) || s.rowLimit < 1)) return err(queryErrors.invalidRowLimit(s.rowLimit));
132
+ if (s.startRow !== void 0 && (!Number.isSafeInteger(s.startRow) || s.startRow < 0)) return err(queryErrors.invalidStartRow(s.startRow));
133
+ const orderBy = normalizeOrderBy(s.orderBy);
134
+ if (s.orderBy !== void 0 && orderBy === void 0) return invalidState("orderBy requires a metric or date column and a consistent asc or desc direction.");
135
+ const filter = parseFilterResult(s.filter);
136
+ if (!filter.ok) return filter;
137
+ const prefilter = parseFilterResult(s.prefilter);
138
+ if (!prefilter.ok) return prefilter;
139
+ return ok({
140
+ dimensions: s.dimensions ?? [],
80
141
  metrics: s.metrics,
81
- filter,
82
- orderBy: normalizeOrderBy(s.orderBy),
142
+ filter: filter.value,
143
+ prefilter: prefilter.value,
144
+ orderBy,
83
145
  rowLimit: s.rowLimit,
84
146
  startRow: s.startRow,
85
147
  dataState: s.dataState,
86
- aggregationType: s.aggregationType
87
- };
88
- if (typeof s.searchType === "string" && KNOWN_SEARCH_TYPES.has(s.searchType)) normalized.searchType = s.searchType;
89
- return ok(normalized);
148
+ aggregationType: s.aggregationType,
149
+ searchType: s.searchType
150
+ });
90
151
  }
91
152
  function normalizeBuilderState(state) {
92
153
  return unwrapResult(normalizeBuilderStateResult(state), queryErrorToException);
@@ -167,7 +228,10 @@ function extractSearchType(state) {
167
228
  if (typeof raw !== "string" || raw.length === 0) return void 0;
168
229
  return KNOWN_SEARCH_TYPES.has(raw) ? raw : void 0;
169
230
  }
170
- function resolveToBodyResult(state) {
231
+ function resolveToBodyResult(input) {
232
+ const parsed = normalizeBuilderStateResult(input);
233
+ if (!parsed.ok) return parsed;
234
+ const state = parsed.value;
171
235
  const { startDate, endDate, searchType, dimensionFilter } = extractSpecialFilters(state.filter);
172
236
  if (!startDate || !endDate) return err(queryErrors.missingDateRange());
173
237
  const body = {
@@ -1,6 +1,6 @@
1
1
  import { GscAggregationType, GscDataState } from "../contracts.mjs";
2
2
  import { Country, Device, SearchType } from "./constants.mjs";
3
- interface DimensionValueMap {
3
+ export interface DimensionValueMap {
4
4
  query: string;
5
5
  queryCanonical: string;
6
6
  page: string;
@@ -11,53 +11,53 @@ interface DimensionValueMap {
11
11
  /** Hour bucket — ISO-8601 with PT offset, e.g. `2025-07-14T13:00:00-07:00`. Use with `dataState: 'hourly_all'`. */
12
12
  hour: string;
13
13
  }
14
- type Dimension = keyof DimensionValueMap;
15
- interface QueryParamValueMap {
14
+ export type Dimension = keyof DimensionValueMap;
15
+ export interface QueryParamValueMap {
16
16
  searchType: SearchType;
17
17
  }
18
- type QueryParamName = keyof QueryParamValueMap;
19
- interface Column<D extends Dimension> {
18
+ export type QueryParamName = keyof QueryParamValueMap;
19
+ export interface Column<D extends Dimension> {
20
20
  readonly __columnBrand: 'gscdump.Column';
21
21
  readonly dimension: D;
22
22
  }
23
- interface QueryParam<P extends QueryParamName> {
23
+ export interface QueryParam<P extends QueryParamName> {
24
24
  readonly __queryParamBrand: 'gscdump.QueryParam';
25
25
  readonly param: P;
26
26
  }
27
- type FilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
28
- type DateOperator = 'gte' | 'gt' | 'lte' | 'lt' | 'between';
29
- type MetricOperator = 'metricGte' | 'metricGt' | 'metricLte' | 'metricLt' | 'metricBetween';
30
- type SpecialOperator = 'topLevel';
31
- interface InternalFilter {
27
+ export type FilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
28
+ export type DateOperator = 'gte' | 'gt' | 'lte' | 'lt' | 'between';
29
+ export type MetricOperator = 'metricGte' | 'metricGt' | 'metricLte' | 'metricLt' | 'metricBetween';
30
+ export type SpecialOperator = 'topLevel';
31
+ export interface InternalFilter {
32
32
  dimension: Dimension | QueryParamName | Metric;
33
33
  operator: FilterOperator | DateOperator | MetricOperator | SpecialOperator;
34
34
  expression: string;
35
35
  expression2?: string;
36
36
  }
37
- interface Filter<C = object> {
37
+ export interface Filter<C = object> {
38
38
  readonly __filterBrand: 'gscdump.Filter';
39
39
  readonly _constraints: C;
40
40
  readonly _filters: InternalFilter[];
41
41
  readonly _nestedGroups?: Filter<any>[];
42
42
  readonly _groupType?: 'and' | 'or';
43
43
  }
44
- type MergeConstraints<F extends Filter<any>[]> = UnionToIntersection<F[number]['_constraints']>;
45
- type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
46
- interface GSCResult<D extends Dimension[], C> {
44
+ export type MergeConstraints<F extends Filter<any>[]> = UnionToIntersection<F[number]['_constraints']>;
45
+ export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
46
+ export interface GSCResult<D extends Dimension[], C> {
47
47
  rows: Array<GSCRow<D, C>>;
48
48
  }
49
- type GSCRow<D extends Dimension[], C> = { [K in D[number]]: K extends keyof C ? C[K] : DimensionValueMap[K]; } & {
49
+ export type GSCRow<D extends Dimension[], C> = { [K in D[number]]: K extends keyof C ? C[K] : DimensionValueMap[K]; } & {
50
50
  clicks: number;
51
51
  impressions: number;
52
52
  ctr: number;
53
53
  position: number;
54
54
  };
55
- type Metric = 'clicks' | 'impressions' | 'ctr' | 'position';
56
- interface MetricColumn<M extends Metric> {
55
+ export type Metric = 'clicks' | 'impressions' | 'ctr' | 'position';
56
+ export interface MetricColumn<M extends Metric> {
57
57
  readonly __metricColumnBrand: 'gscdump.MetricColumn';
58
58
  readonly metric: M;
59
59
  }
60
- interface BuilderState {
60
+ export interface BuilderState {
61
61
  dimensions: Dimension[];
62
62
  metrics?: Metric[];
63
63
  filter?: Filter<any>;
@@ -75,16 +75,15 @@ interface BuilderState {
75
75
  /** GSC search corpus. Wins over any `searchType` filter when both are set. */
76
76
  searchType?: SearchType;
77
77
  }
78
- interface JsonInternalFilter {
78
+ export interface JsonInternalFilter {
79
79
  dimension: string;
80
80
  operator: string;
81
81
  expression: string;
82
82
  expression2?: string;
83
83
  }
84
- interface JsonFilter {
84
+ export interface JsonFilter {
85
85
  _filters: JsonInternalFilter[];
86
86
  _nestedGroups?: JsonFilter[];
87
87
  _groupType?: 'and' | 'or';
88
88
  }
89
- type FilterInput = Filter<any> | JsonFilter;
90
- export { BuilderState, Column, DateOperator, Dimension, DimensionValueMap, Filter, FilterInput, FilterOperator, GSCResult, GSCRow, InternalFilter, JsonFilter, JsonInternalFilter, MergeConstraints, Metric, MetricColumn, MetricOperator, QueryParam, QueryParamName, QueryParamValueMap, SpecialOperator, UnionToIntersection };
89
+ export type FilterInput = Filter<any> | JsonFilter;
@@ -1,3 +1,2 @@
1
- declare function currentPstDate(): string;
2
- declare function daysAgoPst(n: number): string;
3
- export { currentPstDate, daysAgoPst };
1
+ export declare function currentPstDate(): string;
2
+ export declare function daysAgoPst(n: number): string;
@@ -1,31 +1,30 @@
1
- type SitemapIdentityResult = {
1
+ export type SitemapIdentityResult = {
2
2
  _tag: 'ok';
3
3
  url: string;
4
4
  } | {
5
5
  _tag: 'invalid';
6
6
  reason: 'credentials' | 'empty' | 'invalid_url' | 'off_origin';
7
7
  };
8
- interface SitemapEvidenceRecord {
8
+ export interface SitemapEvidenceRecord {
9
9
  path: string;
10
10
  lastDownloaded?: string | null;
11
11
  fetchedAt?: number | null;
12
12
  }
13
- interface ScopedSitemapRecords<T extends SitemapEvidenceRecord> {
13
+ export interface ScopedSitemapRecords<T extends SitemapEvidenceRecord> {
14
14
  sitemaps: Array<T & {
15
15
  path: string;
16
16
  }>;
17
17
  excludedCount: number;
18
18
  duplicateCount: number;
19
19
  }
20
- interface SitemapContentEntry {
20
+ export interface SitemapContentEntry {
21
21
  loc: string;
22
22
  }
23
- declare function canonicalSitemapIdentity(path: string, site?: string): SitemapIdentityResult;
24
- declare function sameSitemapIdentity(left: string, right: string, site?: string): boolean;
25
- declare function scopeSitemapRecords<T extends SitemapEvidenceRecord>(records: readonly T[], site?: string): ScopedSitemapRecords<T>;
23
+ export declare function canonicalSitemapIdentity(path: string, site?: string): SitemapIdentityResult;
24
+ export declare function sameSitemapIdentity(left: string, right: string, site?: string): boolean;
25
+ export declare function scopeSitemapRecords<T extends SitemapEvidenceRecord>(records: readonly T[], site?: string): ScopedSitemapRecords<T>;
26
26
  /**
27
27
  * Versioned exact membership digest. This deliberately does not use the
28
28
  * analytics-only `urlMatchKey` normalizer.
29
29
  */
30
- declare function sitemapContentHash(entries: readonly SitemapContentEntry[]): Promise<string>;
31
- export { ScopedSitemapRecords, SitemapContentEntry, SitemapEvidenceRecord, SitemapIdentityResult, canonicalSitemapIdentity, sameSitemapIdentity, scopeSitemapRecords, sitemapContentHash };
30
+ export declare function sitemapContentHash(entries: readonly SitemapContentEntry[]): Promise<string>;
package/dist/tenant.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- declare function encodeSiteId(siteUrl: string): string;
1
+ export declare function encodeSiteId(siteUrl: string): string;
2
2
  /**
3
3
  * Best-effort inverse of `encodeSiteId` for the common prefixes. Lossy
4
4
  * (`encodeSiteId` collapses non-word chars to `_` and strips trailing
@@ -8,11 +8,10 @@ declare function encodeSiteId(siteUrl: string): string;
8
8
  * Returns the input unchanged when neither prefix is recognised, so callers
9
9
  * can pass through canonical site URLs without branching.
10
10
  */
11
- declare function decodeSiteId(encoded: string): string;
11
+ export declare function decodeSiteId(encoded: string): string;
12
12
  /**
13
13
  * Normalize a siteUrl to the form Google APIs expect: domain properties get
14
14
  * the `sc-domain:` prefix added if missing, URL properties pass through.
15
15
  * Idempotent — safe to call on already-prefixed values.
16
16
  */
17
- declare function normalizeSiteUrl(siteUrl: string): string;
18
- export { decodeSiteId, encodeSiteId, normalizeSiteUrl };
17
+ export declare function normalizeSiteUrl(siteUrl: string): string;
package/dist/url.d.mts CHANGED
@@ -5,5 +5,4 @@
5
5
  *
6
6
  * Example: `https://www.Example.com/Foo/` → `example.com/foo`
7
7
  */
8
- declare function urlMatchKey(url: string): string | null;
9
- export { urlMatchKey };
8
+ export declare function urlMatchKey(url: string): string | null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gscdump",
3
3
  "type": "module",
4
- "version": "3.5.0",
4
+ "version": "3.6.0",
5
5
  "description": "Direct Google Search Console and Bing Webmaster clients with typed queries and Indexing Evidence",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -113,7 +113,7 @@
113
113
  "node": ">=22"
114
114
  },
115
115
  "dependencies": {
116
- "@gscdump/contracts": "^3.5.0",
116
+ "@gscdump/contracts": "^3.6.0",
117
117
  "ofetch": "^1.5.1"
118
118
  },
119
119
  "scripts": {