@gscdump/nuxt 0.22.1 → 0.22.4

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.
@@ -0,0 +1,18 @@
1
+ import type { NuxtRpcErrorEvent, UseNuxtRpcOptions } from 'nuxt-use-query/rpc'
2
+ import { toHumanNuxtRpcError, useNuxtRpc } from 'nuxt-use-query/rpc'
3
+ import { useGscFetch } from './gsc-fetch'
4
+
5
+ // Layer-shared RPC executor. Routes through `useGscFetch()` so auth headers and
6
+ // the configured `apiBase` (cross-origin gscdump.com deployments) are honoured.
7
+ // Defaults `onError` to log a humanised message via `toHumanNuxtRpcError`; the
8
+ // toast is already emitted by `useGscFetch`'s `onResponseError`, so this is
9
+ // non-duplicate diagnostic output that hosts can override.
10
+ export function useGscRpc(options: UseNuxtRpcOptions = {}): ReturnType<typeof useNuxtRpc> {
11
+ return useNuxtRpc({
12
+ ...options,
13
+ fetch: options.fetch ?? (useGscFetch() as UseNuxtRpcOptions['fetch']),
14
+ onError: options.onError ?? ((event: NuxtRpcErrorEvent) => {
15
+ console.warn(`[gsc-rpc] ${event.operation.method} ${event.operation.path}: ${toHumanNuxtRpcError(event.error)}`)
16
+ }),
17
+ })
18
+ }
package/module.ts CHANGED
@@ -67,7 +67,7 @@ export default defineNuxtModule<ModuleOptions>({
67
67
  })
68
68
  }
69
69
 
70
- // Strip underscore-prefixed composables from the consumer auto-import surface.
70
+ // Strip internal composables from the consumer auto-import surface.
71
71
  // `app/composables/_useFoo.ts` stays usable inside the layer via explicit
72
72
  // relative imports, but never appears as a global auto-import in host apps.
73
73
  nuxt.hook('imports:extend', (imports) => {
package/nuxt.config.ts CHANGED
@@ -25,6 +25,26 @@ export default defineNuxtConfig({
25
25
  'nuxt-use-query',
26
26
  ],
27
27
 
28
+ // Enforce shared/contracts + app/queries pattern at build time so host apps
29
+ // can't drift back to hardcoded `/api/*` literals in components / composables.
30
+ // Hosts can opt out per-site by overriding `nuxtUseQuery.contracts.enabled`.
31
+ nuxtUseQuery: {
32
+ contracts: {
33
+ enabled: true,
34
+ // `@gscdump/contracts` is the shared package alias for hosted-API wire
35
+ // shapes — query files must import from there (or a host
36
+ // `shared/contracts/*`).
37
+ apiPrefixes: ['/api/__gsc', '/api/sync-progress'],
38
+ queryDirs: ['app/queries', 'layers/*/app/queries'],
39
+ contractDirs: ['shared/contracts', 'layers/*/shared/contracts', '@gscdump/contracts'],
40
+ serverApiDirs: ['server/api', 'layers/*/server/api'],
41
+ requireServerContracts: false,
42
+ // The config file declares these prefixes so the scanner must skip it.
43
+ // (overrides the default ignore set — keep the defaults alongside it.)
44
+ ignore: ['.git', '.nuxt', '.output', 'coverage', 'dist', 'node_modules', 'nuxt.config.ts'],
45
+ },
46
+ },
47
+
28
48
  css: [
29
49
  fileURLToPath(new URL('./app/assets/css/main.css', import.meta.url)),
30
50
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/nuxt",
3
3
  "type": "module",
4
- "version": "0.22.1",
4
+ "version": "0.22.4",
5
5
  "description": "Nuxt layer: GSC analytics UI + DuckDB-WASM integration. Frontend-only; server primitives live in @gscdump/analysis, @gscdump/engine-sqlite, @gscdump/cloudflare, and host apps.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -30,6 +30,136 @@
30
30
  "types": "./module.ts",
31
31
  "import": "./module.ts",
32
32
  "default": "./module.ts"
33
+ },
34
+ "./queries/gsc": {
35
+ "types": "./app/queries/gsc.ts",
36
+ "import": "./app/queries/gsc.ts",
37
+ "default": "./app/queries/gsc.ts"
38
+ },
39
+ "./composables/useGscAnalytics": {
40
+ "types": "./app/composables/useGscAnalytics.ts",
41
+ "import": "./app/composables/useGscAnalytics.ts",
42
+ "default": "./app/composables/useGscAnalytics.ts"
43
+ },
44
+ "./composables/useGscAnalyticsClient": {
45
+ "types": "./app/composables/useGscAnalyticsClient.ts",
46
+ "import": "./app/composables/useGscAnalyticsClient.ts",
47
+ "default": "./app/composables/useGscAnalyticsClient.ts"
48
+ },
49
+ "./composables/useGscAnalyticsConfig": {
50
+ "types": "./app/composables/useGscAnalyticsConfig.ts",
51
+ "import": "./app/composables/useGscAnalyticsConfig.ts",
52
+ "default": "./app/composables/useGscAnalyticsConfig.ts"
53
+ },
54
+ "./composables/useGscAnalyticsSourceInfo": {
55
+ "types": "./app/composables/useGscAnalyticsSourceInfo.ts",
56
+ "import": "./app/composables/useGscAnalyticsSourceInfo.ts",
57
+ "default": "./app/composables/useGscAnalyticsSourceInfo.ts"
58
+ },
59
+ "./composables/useGscAnalyzer": {
60
+ "types": "./app/composables/useGscAnalyzer.ts",
61
+ "import": "./app/composables/useGscAnalyzer.ts",
62
+ "default": "./app/composables/useGscAnalyzer.ts"
63
+ },
64
+ "./composables/useGscAnalyzerBatch": {
65
+ "types": "./app/composables/useGscAnalyzerBatch.ts",
66
+ "import": "./app/composables/useGscAnalyzerBatch.ts",
67
+ "default": "./app/composables/useGscAnalyzerBatch.ts"
68
+ },
69
+ "./composables/useGscAnalyzerDefs": {
70
+ "types": "./app/composables/useGscAnalyzerDefs.ts",
71
+ "import": "./app/composables/useGscAnalyzerDefs.ts",
72
+ "default": "./app/composables/useGscAnalyzerDefs.ts"
73
+ },
74
+ "./composables/useGscAuth": {
75
+ "types": "./app/composables/useGscAuth.ts",
76
+ "import": "./app/composables/useGscAuth.ts",
77
+ "default": "./app/composables/useGscAuth.ts"
78
+ },
79
+ "./composables/useGscConsoleUrl": {
80
+ "types": "./app/composables/useGscConsoleUrl.ts",
81
+ "import": "./app/composables/useGscConsoleUrl.ts",
82
+ "default": "./app/composables/useGscConsoleUrl.ts"
83
+ },
84
+ "./composables/useGscCountries": {
85
+ "types": "./app/composables/useGscCountries.ts",
86
+ "import": "./app/composables/useGscCountries.ts",
87
+ "default": "./app/composables/useGscCountries.ts"
88
+ },
89
+ "./composables/useGscCurrentSite": {
90
+ "types": "./app/composables/useGscCurrentSite.ts",
91
+ "import": "./app/composables/useGscCurrentSite.ts",
92
+ "default": "./app/composables/useGscCurrentSite.ts"
93
+ },
94
+ "./composables/useGscInspectionHistory": {
95
+ "types": "./app/composables/useGscInspectionHistory.ts",
96
+ "import": "./app/composables/useGscInspectionHistory.ts",
97
+ "default": "./app/composables/useGscInspectionHistory.ts"
98
+ },
99
+ "./composables/useGscInspections": {
100
+ "types": "./app/composables/useGscInspections.ts",
101
+ "import": "./app/composables/useGscInspections.ts",
102
+ "default": "./app/composables/useGscInspections.ts"
103
+ },
104
+ "./composables/useGscPanelContext": {
105
+ "types": "./app/composables/useGscPanelContext.ts",
106
+ "import": "./app/composables/useGscPanelContext.ts",
107
+ "default": "./app/composables/useGscPanelContext.ts"
108
+ },
109
+ "./composables/useGscPeriod": {
110
+ "types": "./app/composables/useGscPeriod.ts",
111
+ "import": "./app/composables/useGscPeriod.ts",
112
+ "default": "./app/composables/useGscPeriod.ts"
113
+ },
114
+ "./composables/useGscQuery": {
115
+ "types": "./app/composables/useGscQuery.ts",
116
+ "import": "./app/composables/useGscQuery.ts",
117
+ "default": "./app/composables/useGscQuery.ts"
118
+ },
119
+ "./composables/useGscQueryDispatcher": {
120
+ "types": "./app/composables/useGscQueryDispatcher.ts",
121
+ "import": "./app/composables/useGscQueryDispatcher.ts",
122
+ "default": "./app/composables/useGscQueryDispatcher.ts"
123
+ },
124
+ "./composables/useGscRequestIndexingInspect": {
125
+ "types": "./app/composables/useGscRequestIndexingInspect.ts",
126
+ "import": "./app/composables/useGscRequestIndexingInspect.ts",
127
+ "default": "./app/composables/useGscRequestIndexingInspect.ts"
128
+ },
129
+ "./composables/useGscResource": {
130
+ "types": "./app/composables/useGscResource.ts",
131
+ "import": "./app/composables/useGscResource.ts",
132
+ "default": "./app/composables/useGscResource.ts"
133
+ },
134
+ "./composables/useGscRollup": {
135
+ "types": "./app/composables/useGscRollup.ts",
136
+ "import": "./app/composables/useGscRollup.ts",
137
+ "default": "./app/composables/useGscRollup.ts"
138
+ },
139
+ "./composables/useGscSearchAppearance": {
140
+ "types": "./app/composables/useGscSearchAppearance.ts",
141
+ "import": "./app/composables/useGscSearchAppearance.ts",
142
+ "default": "./app/composables/useGscSearchAppearance.ts"
143
+ },
144
+ "./composables/useGscSiteAnalyzer": {
145
+ "types": "./app/composables/useGscSiteAnalyzer.ts",
146
+ "import": "./app/composables/useGscSiteAnalyzer.ts",
147
+ "default": "./app/composables/useGscSiteAnalyzer.ts"
148
+ },
149
+ "./composables/useGscSitemapHistory": {
150
+ "types": "./app/composables/useGscSitemapHistory.ts",
151
+ "import": "./app/composables/useGscSitemapHistory.ts",
152
+ "default": "./app/composables/useGscSitemapHistory.ts"
153
+ },
154
+ "./composables/useGscSitemaps": {
155
+ "types": "./app/composables/useGscSitemaps.ts",
156
+ "import": "./app/composables/useGscSitemaps.ts",
157
+ "default": "./app/composables/useGscSitemaps.ts"
158
+ },
159
+ "./composables/useGscTableState": {
160
+ "types": "./app/composables/useGscTableState.ts",
161
+ "import": "./app/composables/useGscTableState.ts",
162
+ "default": "./app/composables/useGscTableState.ts"
33
163
  }
34
164
  },
35
165
  "main": "./nuxt.config.ts",
@@ -53,15 +183,16 @@
53
183
  "@vueuse/core": "^14.3.0",
54
184
  "@vueuse/nuxt": "^14.3.0",
55
185
  "defu": "^6.1.7",
56
- "nuxt-use-query": "^0.1.0",
186
+ "nuxt-use-query": "^0.2.4",
57
187
  "ofetch": "^1.5.1",
58
188
  "tailwindcss": "^4.3.0",
59
- "@gscdump/analysis": "0.22.1",
60
- "@gscdump/contracts": "0.22.1",
61
- "@gscdump/engine": "0.22.1",
62
- "@gscdump/engine-duckdb-wasm": "0.22.1",
63
- "@gscdump/sdk": "0.22.1",
64
- "gscdump": "0.22.1"
189
+ "zod": "^4.4.3",
190
+ "@gscdump/analysis": "0.22.4",
191
+ "@gscdump/contracts": "0.22.4",
192
+ "@gscdump/engine": "0.22.4",
193
+ "@gscdump/engine-duckdb-wasm": "0.22.4",
194
+ "@gscdump/sdk": "0.22.4",
195
+ "gscdump": "0.22.4"
65
196
  },
66
197
  "devDependencies": {
67
198
  "@nuxt/kit": "^4.4.6",
package/types.ts CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  import type { AnalyticsClient } from '@gscdump/sdk'
10
10
  import type { $Fetch } from 'ofetch'
11
- import type { GscQueryDispatcher } from './app/composables/_useGscQueryDispatcher'
12
11
  import type { GscAnalyticsContext } from './app/composables/useGscAnalytics'
13
12
  import type { GscAnalyzerDefinition } from './app/composables/useGscAnalyzerDefs'
13
+ import type { GscQueryDispatcher } from './app/composables/useGscQueryDispatcher'
14
14
 
15
15
  export interface GscAnalyticsRuntimeConfig {
16
16
  /**
@@ -70,10 +70,6 @@ export { gscPanelRunnerKey, useGscPanelRunner } from './app/composables/useGscPa
70
70
 
71
71
  export type { GscPanelRunnerContext } from './app/composables/useGscPanelContext'
72
72
 
73
- export type {
74
- UseGscParquetTableOptions,
75
- UseGscParquetTableReturn,
76
- } from './app/composables/useGscParquetTable'
77
73
  export type {
78
74
  GscBackfillRunner,
79
75
  GscQueryDecision,
@@ -1,198 +0,0 @@
1
- // Parquet-attached DuckDB table view: fuzzy-search + sortable metrics +
2
- // pagination over a single attached parquet (`main.<table>`), date-filtered.
3
- //
4
- // Fires SQL against a parquet view in the browser-attached DuckDB-WASM
5
- // runtime. Two callers today (analyze.vue's `pages` and `keywords` raw
6
- // tabs); third+ callers are mechanical.
7
- //
8
- // Caller passes the `query` fn from `useGscAnalyzer(siteId)`; the composable
9
- // owns the SQL builder, the debounce, the watcher, and the count+page
10
- // Promise.all. It does NOT own `period`/`sort`/`page`/`q` — those are passed
11
- // in so callers can deep-link and share state (the `/analyze` page wires one
12
- // `useGscTableState` across raw + analyzer tabs).
13
-
14
- import type { MaybeRefOrGetter, Ref } from '@vue/runtime-core'
15
- import type { GscSortState } from './useGscTableState'
16
-
17
- interface QueryResult { rows: Record<string, unknown>[], queryMs: number }
18
-
19
- export interface UseGscParquetTableOptions {
20
- /** Attached view name in `main.<table>` (e.g. `pages`, `keywords`). */
21
- table: MaybeRefOrGetter<string>
22
- /** Dimension column rows group by + fuzzy-search against. */
23
- dim: MaybeRefOrGetter<string>
24
- /** `useGscAnalyzer(siteId).query`. */
25
- query: (sql: string, params?: unknown[]) => Promise<QueryResult>
26
- /** Date window applied as `date BETWEEN ? AND ?`. */
27
- dateRange: MaybeRefOrGetter<{ start: string, end: string }>
28
- /**
29
- * External table state — search, sort, page, pageSize. Usually a shared
30
- * `useGscTableState()` so URL deep-links survive tab switches.
31
- */
32
- q: Ref<string>
33
- sort: Ref<GscSortState | null>
34
- page: Ref<number>
35
- pageSize: Ref<number>
36
- /** Gate firing the query until the underlying runtime is ready. */
37
- ready: MaybeRefOrGetter<boolean>
38
- /** Re-fire whenever any of these change. (Period state, stableData flag, …) */
39
- triggers?: MaybeRefOrGetter<unknown>[]
40
- /** Search debounce. Default 200ms. */
41
- debounceMs?: number
42
- }
43
-
44
- export interface UseGscParquetTableReturn {
45
- rows: Ref<Record<string, unknown>[]>
46
- totalRows: Ref<number | null>
47
- totalPages: Ref<number | null>
48
- queryMs: Ref<number | null>
49
- loading: Ref<boolean>
50
- error: Ref<string | null>
51
- }
52
-
53
- const SQL_SORT_COLS = new Set(['clicks', 'impressions', 'ctr', 'avg_position'])
54
- const WHITESPACE_RE = /\s+/
55
-
56
- function tokenize(s: string): string[] {
57
- const seen = new Set<string>()
58
- const out: string[] = []
59
- for (const t of s.trim().split(WHITESPACE_RE).filter(Boolean)) {
60
- const lower = t.toLowerCase()
61
- if (!seen.has(lower)) {
62
- seen.add(lower)
63
- out.push(lower)
64
- }
65
- }
66
- return out
67
- }
68
-
69
- interface SearchClauses { where: string, rank: string, params: unknown[] }
70
-
71
- function buildSearchClauses(dim: string, tokens: string[]): SearchClauses {
72
- if (tokens.length === 0)
73
- return { where: '', rank: '', params: [] }
74
- const patterns = tokens.map(t => `%${t}%`)
75
- const ors = tokens.map(() => `${dim} ILIKE ?`).join(' OR ')
76
- const cases = tokens.map(() => `(CASE WHEN ${dim} ILIKE ? THEN 1 ELSE 0 END)`).join(' + ')
77
- return {
78
- where: `WHERE (${ors})`,
79
- rank: `${cases} DESC,`,
80
- params: [...patterns, ...patterns],
81
- }
82
- }
83
-
84
- function withDateFilter(
85
- searchWhere: string,
86
- searchParams: unknown[],
87
- start: string,
88
- end: string,
89
- ): { where: string, params: unknown[] } {
90
- const dateClause = 'date BETWEEN ? AND ?'
91
- const where = searchWhere ? `${searchWhere} AND ${dateClause}` : `WHERE ${dateClause}`
92
- return { where, params: [...searchParams, start, end] }
93
- }
94
-
95
- function sortKey(dim: string, sort: GscSortState | null): string {
96
- const col = sort?.column ?? 'clicks'
97
- if (SQL_SORT_COLS.has(col))
98
- return col
99
- if (col === dim)
100
- return dim
101
- return 'clicks'
102
- }
103
-
104
- export function useGscParquetTable(opts: UseGscParquetTableOptions): UseGscParquetTableReturn {
105
- const rows = ref<Record<string, unknown>[]>([])
106
- const totalRows = ref<number | null>(null)
107
- const queryMs = ref<number | null>(null)
108
- const loading = ref(false)
109
- const error = ref<string | null>(null)
110
-
111
- const searchDebounced = ref('')
112
- let handle: ReturnType<typeof setTimeout> | null = null
113
- watch(opts.q, (v: string) => {
114
- if (handle)
115
- clearTimeout(handle)
116
- handle = setTimeout(() => {
117
- searchDebounced.value = v
118
- }, opts.debounceMs ?? 200)
119
- })
120
-
121
- async function run(): Promise<void> {
122
- if (!toValue(opts.ready))
123
- return
124
- const table = toValue(opts.table)
125
- const dim = toValue(opts.dim)
126
- const { start, end } = toValue(opts.dateRange)
127
- const tokens = tokenize(searchDebounced.value)
128
- const { where: searchWhere, rank, params: searchParams } = buildSearchClauses(dim, tokens)
129
- const dir = (opts.sort.value?.direction ?? 'desc').toUpperCase()
130
- const limit = opts.pageSize.value
131
- const offset = (opts.page.value - 1) * limit
132
-
133
- const wherePart = withDateFilter(searchWhere, searchParams.slice(0, tokens.length), start, end)
134
- const rankParams = searchParams.slice(tokens.length)
135
-
136
- const pageSql = `
137
- SELECT ${dim},
138
- SUM(clicks)::BIGINT AS clicks,
139
- SUM(impressions)::BIGINT AS impressions,
140
- CASE WHEN SUM(impressions) > 0
141
- THEN ROUND(SUM(clicks) * 1.0 / SUM(impressions), 4)
142
- ELSE 0 END AS ctr,
143
- CASE WHEN SUM(impressions) > 0
144
- THEN ROUND(SUM(sum_position) / SUM(impressions) + 1, 2)
145
- ELSE 0 END AS avg_position
146
- FROM main.${table}
147
- ${wherePart.where}
148
- GROUP BY ${dim}
149
- ORDER BY ${rank} ${sortKey(dim, opts.sort.value)} ${dir}
150
- LIMIT ${limit} OFFSET ${offset}
151
- `
152
- const countSql = `SELECT COUNT(DISTINCT ${dim})::BIGINT AS n FROM main.${table} ${wherePart.where}`
153
-
154
- loading.value = true
155
- error.value = null
156
- rows.value = []
157
- queryMs.value = null
158
- try {
159
- const [pageRes, countRes] = await Promise.all([
160
- opts.query(pageSql, [...wherePart.params, ...rankParams]),
161
- opts.query(countSql, wherePart.params),
162
- ])
163
- rows.value = pageRes.rows
164
- queryMs.value = pageRes.queryMs
165
- const n = countRes.rows[0]?.n
166
- totalRows.value = typeof n === 'bigint' ? Number(n) : typeof n === 'number' ? n : null
167
- }
168
- catch (err) {
169
- error.value = err instanceof Error ? err.message : String(err)
170
- }
171
- finally {
172
- loading.value = false
173
- }
174
- }
175
-
176
- const triggerSources = [
177
- () => toValue(opts.ready),
178
- () => toValue(opts.table),
179
- () => toValue(opts.dim),
180
- () => toValue(opts.dateRange),
181
- opts.sort,
182
- opts.page,
183
- opts.pageSize,
184
- searchDebounced,
185
- ...(opts.triggers ?? []).map(t => () => toValue(t as MaybeRefOrGetter<unknown>)),
186
- ]
187
- watch(triggerSources, () => {
188
- void run()
189
- })
190
-
191
- const totalPages = computed(() => {
192
- if (totalRows.value == null)
193
- return null
194
- return Math.max(1, Math.ceil(totalRows.value / opts.pageSize.value))
195
- })
196
-
197
- return { rows, totalRows, totalPages, queryMs, loading, error }
198
- }