@gscdump/nuxt 0.22.4 → 1.5.1

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 (66) hide show
  1. package/README.md +10 -137
  2. package/dist/module.d.mts +22 -0
  3. package/dist/module.mjs +30 -0
  4. package/dist/runtime-config.d.mts +12 -0
  5. package/dist/runtime-config.mjs +27 -0
  6. package/package.json +26 -176
  7. package/app/assets/css/main.css +0 -2
  8. package/app/components/GscAnalyzerPanel.vue +0 -94
  9. package/app/components/GscAnonymizationBanner.vue +0 -46
  10. package/app/components/GscBootProgress.vue +0 -297
  11. package/app/components/GscCommandPalette.vue +0 -77
  12. package/app/components/GscDashboardPage.vue +0 -26
  13. package/app/components/GscEngineBadge.vue +0 -28
  14. package/app/components/GscHero.vue +0 -214
  15. package/app/components/GscLazyTopResult.vue +0 -47
  16. package/app/components/GscPerformanceChart.vue +0 -532
  17. package/app/components/GscPositionDistributionChart.vue +0 -63
  18. package/app/components/GscQueryLabel.vue +0 -63
  19. package/app/components/GscSitePageHeader.vue +0 -40
  20. package/app/components/GscSourceDebugPanel.vue +0 -196
  21. package/app/components/GscSyncStatusCell.vue +0 -54
  22. package/app/components/GscTopRollupCard.vue +0 -90
  23. package/app/composables/_useGscBackfill.ts +0 -119
  24. package/app/composables/_useGscQueryDispatcher.ts +0 -123
  25. package/app/composables/_useGscResource.ts +0 -202
  26. package/app/composables/_useGscSharedSiteResource.ts +0 -136
  27. package/app/composables/useGscAnalytics.ts +0 -206
  28. package/app/composables/useGscAnalyticsClient.ts +0 -9
  29. package/app/composables/useGscAnalyticsConfig.ts +0 -8
  30. package/app/composables/useGscAnalyticsSourceInfo.ts +0 -165
  31. package/app/composables/useGscAnalyzer.ts +0 -460
  32. package/app/composables/useGscAnalyzerBatch.ts +0 -106
  33. package/app/composables/useGscAnalyzerDefs.ts +0 -119
  34. package/app/composables/useGscAuth.ts +0 -115
  35. package/app/composables/useGscConsoleUrl.ts +0 -45
  36. package/app/composables/useGscCountries.ts +0 -46
  37. package/app/composables/useGscCurrentSite.ts +0 -36
  38. package/app/composables/useGscInspectionHistory.ts +0 -42
  39. package/app/composables/useGscInspections.ts +0 -52
  40. package/app/composables/useGscPanelContext.ts +0 -31
  41. package/app/composables/useGscPeriod.ts +0 -243
  42. package/app/composables/useGscQuery.ts +0 -301
  43. package/app/composables/useGscQueryDispatcher.ts +0 -14
  44. package/app/composables/useGscRequestIndexingInspect.ts +0 -29
  45. package/app/composables/useGscResource.ts +0 -11
  46. package/app/composables/useGscRollup.ts +0 -237
  47. package/app/composables/useGscSearchAppearance.ts +0 -46
  48. package/app/composables/useGscSiteAnalyzer.ts +0 -456
  49. package/app/composables/useGscSitemapHistory.ts +0 -41
  50. package/app/composables/useGscSitemaps.ts +0 -45
  51. package/app/composables/useGscTableState.ts +0 -119
  52. package/app/plugins/analytics.ts +0 -57
  53. package/app/queries/gsc.ts +0 -163
  54. package/app/utils/anonymization.ts +0 -24
  55. package/app/utils/country-names.ts +0 -56
  56. package/app/utils/duckdb-wasm.ts +0 -166
  57. package/app/utils/gsc-constants.ts +0 -10
  58. package/app/utils/gsc-error.ts +0 -58
  59. package/app/utils/gsc-fetch.ts +0 -94
  60. package/app/utils/gsc-filters.ts +0 -32
  61. package/app/utils/gsc-rows.ts +0 -72
  62. package/app/utils/gsc-rpc.ts +0 -18
  63. package/app/utils/position.ts +0 -7
  64. package/module.ts +0 -81
  65. package/nuxt.config.ts +0 -61
  66. package/types.ts +0 -115
@@ -1,456 +0,0 @@
1
- // Per-site DuckDB-WASM analyzer over the hosted Iceberg fact tables.
2
- //
3
- // The module owns the browser runtime lifecycle: resolve analysis sources,
4
- // boot shared DuckDB-WASM, attach parquet tables on demand, mirror progress
5
- // into the layer-wide map, and materialise Arrow rows into plain objects.
6
- // Callers keep ownership of SQL, presentation, and page-specific state.
7
-
8
- import type { AsyncDuckDB, AsyncDuckDBConnection } from '@duckdb/duckdb-wasm'
9
- import type { FileResolutionResponse } from '@gscdump/contracts'
10
- import type { OpfsAttachedHandle } from '@gscdump/engine-duckdb-wasm'
11
- import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
12
- import { defaultAnalyzerRegistry } from '@gscdump/analysis'
13
- import { runAnalyzerFromSource } from '@gscdump/engine/analyzer'
14
- import { createAttachedTableSource } from '@gscdump/engine/source'
15
- import { gscQueries } from '../queries/gsc'
16
- import { attachParquetWithFallback, sharedGscDuckDBWasm } from '../utils/duckdb-wasm'
17
- import { useGscRpc } from '../utils/gsc-rpc'
18
-
19
- export type GscFactTable
20
- = | 'pages'
21
- | 'queries'
22
- | 'countries'
23
- | 'dates'
24
- | 'page_queries'
25
- | 'search_appearance'
26
- | 'search_appearance_pages'
27
- | 'search_appearance_queries'
28
- | 'search_appearance_page_queries'
29
-
30
- export type GscTableStage = 'idle' | 'resolving' | 'downloading' | 'attaching' | 'ready' | 'error' | 'unavailable'
31
-
32
- export interface GscTableStatus {
33
- stage: GscTableStage
34
- filesAttached: number
35
- filesTotal: number
36
- startedAt?: number
37
- endedAt?: number
38
- error?: string
39
- }
40
-
41
- export interface UseGscSiteAnalyzerReturn {
42
- tables: Readonly<Ref<Record<GscFactTable, GscTableStatus>>>
43
- ready: Readonly<Ref<boolean>>
44
- error: Readonly<Ref<Error | null>>
45
- query: <T = Record<string, unknown>>(opts: {
46
- sql: string
47
- needs: readonly GscFactTable[]
48
- params?: readonly unknown[]
49
- }) => Promise<T[]>
50
- runQuery: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<{ rows: T[], queryMs: number }>
51
- analyze: (params: AnalysisParams, opts?: { signal?: AbortSignal }) => Promise<AnalysisResult & { queryMs: number }>
52
- }
53
-
54
- interface BootedAnalyzer {
55
- bootPromise: Promise<{ db: AsyncDuckDB, conn: AsyncDuckDBConnection }>
56
- tablePromises: Map<GscFactTable, Promise<void>>
57
- tables: Ref<Record<GscFactTable, GscTableStatus>>
58
- ready: Ref<boolean>
59
- error: Ref<Error | null>
60
- withDb: <T>(fn: (conn: AsyncDuckDBConnection) => Promise<T>) => Promise<T>
61
- refs: number
62
- dispose: () => Promise<void>
63
- }
64
-
65
- interface AnalyzerArgs {
66
- siteId: string
67
- searchType: string
68
- range: { start: string, end: string }
69
- useOpfsCache: boolean
70
- }
71
-
72
- const ALL_TABLES: readonly GscFactTable[] = [
73
- 'pages',
74
- 'queries',
75
- 'countries',
76
- 'dates',
77
- 'page_queries',
78
- 'search_appearance',
79
- 'search_appearance_pages',
80
- 'search_appearance_queries',
81
- 'search_appearance_page_queries',
82
- ] as const
83
-
84
- const FACT_TABLE_NAMES = ALL_TABLES.join('|')
85
- const TABLE_RE = new RegExp(`\\b(?:FROM|JOIN)\\s+(?:main\\.)?(${FACT_TABLE_NAMES})\\b`, 'gi')
86
- const instanceCaches = new WeakMap<object, Map<string, BootedAnalyzer>>()
87
-
88
- function tablesFromSql(sql: string): readonly GscFactTable[] {
89
- const out = new Set<GscFactTable>()
90
- for (const m of sql.matchAll(TABLE_RE))
91
- out.add(m[1]!.toLowerCase() as GscFactTable)
92
- return out.size > 0 ? Array.from(out) : ALL_TABLES
93
- }
94
-
95
- function sqlIdent(s: string): string {
96
- return s.replace(/\W/g, '_')
97
- }
98
-
99
- function emptyStatus(): GscTableStatus {
100
- return { stage: 'idle', filesAttached: 0, filesTotal: 0 }
101
- }
102
-
103
- function emptyTables(): Record<GscFactTable, GscTableStatus> {
104
- return Object.fromEntries(ALL_TABLES.map(t => [t, emptyStatus()])) as Record<GscFactTable, GscTableStatus>
105
- }
106
-
107
- function getCache(): Map<string, BootedAnalyzer> {
108
- const app = useNuxtApp()
109
- let bag = instanceCaches.get(app)
110
- if (!bag) {
111
- bag = new Map()
112
- instanceCaches.set(app, bag)
113
- }
114
- return bag
115
- }
116
-
117
- function instanceKey(siteId: string, searchType: string, range: { start: string, end: string }): string {
118
- return `${siteId}|${searchType}|${range.start}|${range.end}`
119
- }
120
-
121
- export function useGscSiteAnalyzer(
122
- siteId: MaybeRefOrGetter<string | null | undefined>,
123
- range: MaybeRefOrGetter<{ start: string, end: string } | null | undefined>,
124
- options: {
125
- searchType?: 'web' | 'image' | 'video' | 'news' | 'discover' | 'googleNews'
126
- useOpfsCache?: boolean
127
- } = {},
128
- ): UseGscSiteAnalyzerReturn {
129
- const searchType = options.searchType ?? 'web'
130
- const useOpfsCache = options.useOpfsCache ?? true
131
- const tables = ref<Record<GscFactTable, GscTableStatus>>(emptyTables())
132
- const ready = ref(false)
133
- const error = ref<Error | null>(null)
134
- let active: BootedAnalyzer | null = null
135
- let stopEffects: (() => void) | null = null
136
-
137
- function bind(instance: BootedAnalyzer | null): void {
138
- const prev = active
139
- stopEffects?.()
140
- stopEffects = null
141
- active = instance
142
-
143
- if (instance) {
144
- instance.refs++
145
- const scope = effectScope()
146
- scope.run(() => {
147
- watchEffect(() => {
148
- tables.value = instance.tables.value
149
- })
150
- watchEffect(() => {
151
- ready.value = instance.ready.value
152
- })
153
- watchEffect(() => {
154
- error.value = instance.error.value
155
- })
156
- })
157
- stopEffects = () => scope.stop()
158
- }
159
- else {
160
- tables.value = emptyTables()
161
- ready.value = false
162
- error.value = null
163
- }
164
-
165
- if (prev) {
166
- prev.refs--
167
- if (prev.refs <= 0)
168
- prev.dispose().catch(() => {})
169
- }
170
- }
171
-
172
- function obtain(sid: string, r: { start: string, end: string }): BootedAnalyzer {
173
- const cache = getCache()
174
- const key = instanceKey(sid, searchType, r)
175
- const hit = cache.get(key)
176
- if (hit)
177
- return hit
178
- const created = createAnalyzer({ siteId: sid, searchType, range: r, useOpfsCache })
179
- cache.set(key, created)
180
- return created
181
- }
182
-
183
- watch(
184
- () => {
185
- const sid = toValue(siteId) ?? null
186
- const r = toValue(range) ?? null
187
- return sid && r?.start && r?.end ? { sid, r } : null
188
- },
189
- (val) => {
190
- if (!val || !import.meta.client) {
191
- bind(null)
192
- return
193
- }
194
- bind(obtain(val.sid, val.r))
195
- },
196
- { immediate: true },
197
- )
198
-
199
- onScopeDispose(() => bind(null))
200
-
201
- async function query<T = Record<string, unknown>>(opts: {
202
- sql: string
203
- needs: readonly GscFactTable[]
204
- params?: readonly unknown[]
205
- }): Promise<T[]> {
206
- if (!active)
207
- throw new Error('useGscSiteAnalyzer: no active instance (site/range not set)')
208
- const instance = active
209
- await instance.bootPromise
210
- await Promise.all(opts.needs.map(t => ensureTable(instance, t)))
211
- return instance.withDb(async (conn) => {
212
- const result = opts.params && opts.params.length > 0
213
- ? await (async () => {
214
- const stmt = await conn.prepare(opts.sql)
215
- try {
216
- return await stmt.query(...(opts.params as unknown[]))
217
- }
218
- finally {
219
- await stmt.close()
220
- }
221
- })()
222
- : await conn.query(opts.sql)
223
- const arr = result.toArray() as unknown as Array<Record<string, unknown>>
224
- return arr.map(row => ({ ...row })) as unknown as T[]
225
- })
226
- }
227
-
228
- async function runQuery<T = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<{ rows: T[], queryMs: number }> {
229
- const t0 = performance.now()
230
- const rows = await query<T>({ sql, needs: tablesFromSql(sql), params })
231
- return { rows, queryMs: performance.now() - t0 }
232
- }
233
-
234
- async function analyze(params: AnalysisParams, opts?: { signal?: AbortSignal }): Promise<AnalysisResult & { queryMs: number }> {
235
- const t0 = performance.now()
236
- const source = createAttachedTableSource(
237
- {
238
- query: async (sql, bindParams) => {
239
- return query<Record<string, unknown>>({ sql, needs: tablesFromSql(sql), params: bindParams })
240
- },
241
- },
242
- {
243
- schema: 'main',
244
- signal: opts?.signal,
245
- attachedTables: ALL_TABLES as readonly string[],
246
- },
247
- )
248
- const result = await runAnalyzerFromSource(source, params, defaultAnalyzerRegistry)
249
- return {
250
- results: result.results as AnalysisResult['results'],
251
- meta: result.meta as AnalysisResult['meta'],
252
- queryMs: performance.now() - t0,
253
- }
254
- }
255
-
256
- return {
257
- tables: tables as Readonly<Ref<Record<GscFactTable, GscTableStatus>>>,
258
- ready: ready as Readonly<Ref<boolean>>,
259
- error: error as Readonly<Ref<Error | null>>,
260
- query,
261
- runQuery,
262
- analyze,
263
- }
264
- }
265
-
266
- function createAnalyzer(args: AnalyzerArgs): BootedAnalyzer {
267
- const tables = ref<Record<GscFactTable, GscTableStatus>>(emptyTables())
268
- const ready = ref(false)
269
- const error = ref<Error | null>(null)
270
- const tablePromises = new Map<GscFactTable, Promise<void>>()
271
- const opfsHandles = new Map<GscFactTable, OpfsAttachedHandle>()
272
-
273
- const rpc = useGscRpc()
274
- const analyticsConfig = useGscAnalyticsConfig()
275
- const analyticsCtx = useGscAnalyticsContext()
276
-
277
- let resolvePromise: Promise<FileResolutionResponse | null> | null = null
278
- function getResolution(): Promise<FileResolutionResponse | null> {
279
- if (resolvePromise)
280
- return resolvePromise
281
- resolvePromise = rpc.query(
282
- gscQueries.analysisSources(args.siteId, undefined, {
283
- searchType: args.searchType as 'web' | 'image' | 'video' | 'news' | 'discover' | 'googleNews',
284
- start: args.range.start,
285
- end: args.range.end,
286
- }),
287
- { silent: true },
288
- )
289
- .then((res) => {
290
- if (!res || (res as { canUseBrowser?: boolean }).canUseBrowser === false)
291
- return null
292
- return res as unknown as FileResolutionResponse
293
- })
294
- .catch(() => null)
295
- return resolvePromise
296
- }
297
-
298
- const bootPromise: Promise<{ db: AsyncDuckDB, conn: AsyncDuckDBConnection }> = (async () => {
299
- const bundleBase = (analyticsConfig as { duckdbBundleBase?: string }).duckdbBundleBase
300
- const boot = await sharedGscDuckDBWasm(bundleBase)
301
- ready.value = true
302
- return boot
303
- })()
304
- bootPromise.catch((err) => {
305
- error.value = err instanceof Error ? err : new Error(String(err))
306
- })
307
-
308
- let dbLock: Promise<unknown> = Promise.resolve()
309
- function withDb<T>(fn: (conn: AsyncDuckDBConnection) => Promise<T>): Promise<T> {
310
- const next = dbLock.then(async () => {
311
- const { conn } = await bootPromise
312
- return fn(conn)
313
- })
314
- dbLock = next.catch(() => {})
315
- return next
316
- }
317
-
318
- function patchSelf(table: GscFactTable, patch: Partial<GscTableStatus>): void {
319
- tables.value = { ...tables.value, [table]: { ...tables.value[table], ...patch } }
320
- }
321
-
322
- function patchLayer(table: GscFactTable, stage: GscTableStage, extras: Partial<GscTableStatus> = {}): void {
323
- const siteSlot = `${args.siteId}#${table}`
324
- const layerStage = stage === 'unavailable'
325
- ? 'ready'
326
- : stage === 'downloading' || stage === 'attaching'
327
- ? 'attach'
328
- : stage === 'resolving'
329
- ? 'manifest'
330
- : stage === 'ready'
331
- ? 'ready'
332
- : stage === 'error' ? 'error' : 'idle'
333
- analyticsCtx.patchProgress(siteSlot, {
334
- stage: layerStage,
335
- source: 'duckdb',
336
- filesAttached: extras.filesAttached ?? tables.value[table].filesAttached,
337
- filesTotal: extras.filesTotal ?? tables.value[table].filesTotal,
338
- startedAt: extras.startedAt ?? tables.value[table].startedAt ?? Date.now(),
339
- endedAt: extras.endedAt,
340
- error: extras.error,
341
- })
342
- }
343
-
344
- async function attachTable(table: GscFactTable): Promise<void> {
345
- const startedAt = Date.now()
346
- patchSelf(table, { stage: 'resolving', startedAt, endedAt: undefined, error: undefined })
347
- patchLayer(table, 'resolving', { startedAt })
348
-
349
- const full = await getResolution()
350
- if (!full) {
351
- const endedAt = Date.now()
352
- patchSelf(table, { stage: 'unavailable', endedAt })
353
- patchLayer(table, 'unavailable', { endedAt })
354
- return
355
- }
356
-
357
- const tableEntry = full.tables.find(t => t.table === table)
358
- const browserFiles = tableEntry?.mode === 'browser' ? tableEntry.files : []
359
- if (browserFiles.length === 0) {
360
- const endedAt = Date.now()
361
- patchSelf(table, { stage: 'unavailable', endedAt })
362
- patchLayer(table, 'unavailable', { endedAt })
363
- return
364
- }
365
-
366
- patchSelf(table, { stage: 'downloading', filesTotal: browserFiles.length })
367
- patchLayer(table, 'downloading', { filesTotal: browserFiles.length })
368
-
369
- const sid = sqlIdent(args.siteId)
370
- const tableIdent = sqlIdent(table)
371
- const viewName = `${tableIdent}_${sid}`
372
- let filesAttached = 0
373
- const { db, conn } = await bootPromise
374
- const opfsHandle = await attachParquetWithFallback({
375
- db,
376
- conn,
377
- viewName,
378
- files: browserFiles,
379
- version: full.snapshotVersion,
380
- useOpfsCache: args.useOpfsCache,
381
- withDb: fn => withDb(() => fn()),
382
- onFileProgress: () => {
383
- filesAttached++
384
- patchSelf(table, { filesAttached })
385
- patchLayer(table, 'downloading', { filesAttached })
386
- },
387
- }).catch((err) => {
388
- const msg = err instanceof Error ? err.message : String(err)
389
- const endedAt = Date.now()
390
- patchSelf(table, { stage: 'error', error: msg, endedAt })
391
- patchLayer(table, 'error', { error: msg, endedAt })
392
- throw err
393
- })
394
-
395
- if (viewName !== tableIdent) {
396
- await withDb(async (conn) => {
397
- await conn.query(`CREATE OR REPLACE VIEW ${tableIdent} AS SELECT * FROM ${viewName}`)
398
- })
399
- }
400
-
401
- if (opfsHandle)
402
- opfsHandles.set(table, opfsHandle)
403
-
404
- const endedAt = Date.now()
405
- patchSelf(table, { stage: 'ready', endedAt })
406
- patchLayer(table, 'ready', { endedAt })
407
- }
408
-
409
- function ensureTableInner(table: GscFactTable): Promise<void> {
410
- const existing = tablePromises.get(table)
411
- if (existing)
412
- return existing
413
- const p = attachTable(table)
414
- tablePromises.set(table, p)
415
- return p
416
- }
417
-
418
- const instance: BootedAnalyzer = {
419
- bootPromise,
420
- tablePromises,
421
- tables,
422
- ready,
423
- error,
424
- withDb,
425
- refs: 0,
426
- dispose: async () => {
427
- const cache = instanceCaches.get(useNuxtApp())
428
- if (cache) {
429
- for (const [k, v] of cache.entries()) {
430
- if (v === instance) {
431
- cache.delete(k)
432
- break
433
- }
434
- }
435
- }
436
- try {
437
- for (const h of opfsHandles.values())
438
- await h.detach().catch(() => {})
439
- opfsHandles.clear()
440
- const { conn } = await bootPromise
441
- for (const t of ALL_TABLES)
442
- await conn.query(`DROP VIEW IF EXISTS ${sqlIdent(t)}`).catch(() => {})
443
- }
444
- catch {
445
- // Boot failed; nothing to tear down.
446
- }
447
- },
448
- }
449
- ;(instance as BootedAnalyzer & { ensureTable: typeof ensureTableInner }).ensureTable = ensureTableInner
450
- return instance
451
- }
452
-
453
- async function ensureTable(instance: BootedAnalyzer, table: GscFactTable): Promise<void> {
454
- const ensure = (instance as BootedAnalyzer & { ensureTable: (t: GscFactTable) => Promise<void> }).ensureTable
455
- return ensure(table)
456
- }
@@ -1,41 +0,0 @@
1
- // Fetches the snapshot timeline for a single sitemap (by feedpath hash)
2
- // from the site's history store. Oldest → newest.
3
-
4
- import type { SitemapHistoryRecord, SitemapHistoryResponse } from '@gscdump/contracts'
5
- import type { ComputedRef, Ref } from '@vue/runtime-core'
6
- import type { GscResourceStatus } from './_useGscResource'
7
- import { gscQueries } from '../queries/gsc'
8
- import { useGscResource } from './_useGscResource'
9
-
10
- export interface UseGscSitemapHistoryReturn {
11
- response: Readonly<Ref<SitemapHistoryResponse | null>>
12
- snapshots: ComputedRef<SitemapHistoryRecord[]>
13
- path: ComputedRef<string | null>
14
- loading: ComputedRef<boolean>
15
- status: Readonly<Ref<GscResourceStatus>>
16
- error: Readonly<Ref<Error | null>>
17
- refresh: () => Promise<void>
18
- }
19
-
20
- export function useGscSitemapHistory(
21
- siteId: MaybeRefOrGetter<string | null | undefined>,
22
- feedpathHash: MaybeRefOrGetter<string | null | undefined>,
23
- ): UseGscSitemapHistoryReturn {
24
- const { data, status, loading, error, refresh } = useGscResource<[string, string], SitemapHistoryResponse>({
25
- keys: [siteId, feedpathHash] as const,
26
- operation: (id, hash) => gscQueries.sitemapHistory(id, hash),
27
- })
28
-
29
- const snapshots = computed(() => data.value?.snapshots ?? [])
30
- const path = computed(() => data.value?.path ?? null)
31
-
32
- return {
33
- response: data as Readonly<Ref<SitemapHistoryResponse | null>>,
34
- snapshots,
35
- path,
36
- loading,
37
- status,
38
- error,
39
- refresh,
40
- }
41
- }
@@ -1,45 +0,0 @@
1
- // Fetches the sitemap entity index for a site. Records are sorted by
2
- // `lastDownloaded` desc. Split from the previous `useEntity(kind)` discriminated
3
- // API so each entity stays its own typed hook.
4
-
5
- import type { SitemapHistoryRecord, SitemapIndex } from '@gscdump/contracts'
6
- import type { ComputedRef, Ref } from '@vue/runtime-core'
7
- import type { GscResourceStatus } from './_useGscResource'
8
- import { gscQueries } from '../queries/gsc'
9
- import { useGscResource } from './_useGscResource'
10
-
11
- export interface UseGscSitemapsReturn {
12
- index: Readonly<Ref<SitemapIndex | null>>
13
- records: ComputedRef<SitemapHistoryRecord[]>
14
- loading: ComputedRef<boolean>
15
- status: Readonly<Ref<GscResourceStatus>>
16
- error: Readonly<Ref<Error | null>>
17
- refresh: () => Promise<void>
18
- }
19
-
20
- export function useGscSitemaps(siteId: MaybeRefOrGetter<string | null | undefined>): UseGscSitemapsReturn {
21
- const { data, status, loading, error, refresh } = useGscResource<[string], SitemapIndex>({
22
- keys: [siteId] as const,
23
- operation: (id: string) => gscQueries.sitemaps(id),
24
- })
25
-
26
- const records = computed<SitemapHistoryRecord[]>(() => {
27
- if (!data.value)
28
- return []
29
- const list = Object.values(data.value.records) as SitemapHistoryRecord[]
30
- return list.sort((a, b) => {
31
- const ad = a.lastDownloaded ?? ''
32
- const bd = b.lastDownloaded ?? ''
33
- return bd.localeCompare(ad)
34
- })
35
- })
36
-
37
- return {
38
- index: data as Readonly<Ref<SitemapIndex | null>>,
39
- records,
40
- loading,
41
- status,
42
- error,
43
- refresh,
44
- }
45
- }
@@ -1,119 +0,0 @@
1
- // Reactive table state — search, pagination, sort, filter — with optional
2
- // URL sync. One source of truth across a page so deep-links and
3
- // back/forward navigation restore exactly what the user was looking at.
4
-
5
- export interface GscSortState {
6
- column: string
7
- direction: 'asc' | 'desc'
8
- }
9
-
10
- export interface UseGscTableStateOptions<TFilter = Record<string, string>> {
11
- /** Sync state into the URL query string. Default `true`. */
12
- syncUrl?: boolean
13
- /** Prefix for query keys (avoids collisions when multiple tables share a page). */
14
- prefix?: string
15
- defaultPage?: number
16
- defaultPageSize?: number
17
- defaultQ?: string
18
- defaultSort?: GscSortState | null
19
- defaultFilter?: TFilter
20
- }
21
-
22
- export interface UseGscTableStateReturn<TFilter = Record<string, string>> {
23
- q: Ref<string>
24
- page: Ref<number>
25
- pageSize: Ref<number>
26
- sort: Ref<GscSortState | null>
27
- filter: Ref<TFilter>
28
- reset: () => void
29
- /** Toggle a column's sort: desc → asc → off. */
30
- toggleSort: (column: string) => void
31
- }
32
-
33
- export function useGscTableState<TFilter extends Record<string, any> = Record<string, string>>(
34
- opts: UseGscTableStateOptions<TFilter> = {},
35
- ): UseGscTableStateReturn<TFilter> {
36
- const syncUrl = opts.syncUrl ?? true
37
- const prefix = opts.prefix ?? ''
38
- const k = (name: string): string => (prefix ? `${prefix}_${name}` : name)
39
-
40
- const defaultPage = opts.defaultPage ?? 1
41
- const defaultPageSize = opts.defaultPageSize ?? 25
42
- const defaultQ = opts.defaultQ ?? ''
43
- const defaultSort = opts.defaultSort ?? null
44
- const defaultFilter = (opts.defaultFilter ?? {}) as TFilter
45
-
46
- const route = syncUrl ? useRoute() : null
47
- const router = syncUrl ? useRouter() : null
48
-
49
- function readQuery<T>(key: string, fallback: T, parse: (raw: string) => T): T {
50
- if (!route)
51
- return fallback
52
- const raw = route.query[k(key)]
53
- if (raw == null)
54
- return fallback
55
- return parse(Array.isArray(raw) ? (raw[0] ?? '') : String(raw))
56
- }
57
-
58
- function writeQuery(key: string, value: string | null): void {
59
- if (!route || !router)
60
- return
61
- const query = { ...route.query }
62
- const fullKey = k(key)
63
- if (value == null || value === '')
64
- delete query[fullKey]
65
- else
66
- query[fullKey] = value
67
- void router.replace({ query })
68
- }
69
-
70
- const q = ref(readQuery('q', defaultQ, s => s))
71
- const page = ref(readQuery('page', defaultPage, s => Number(s) || defaultPage))
72
- const pageSize = ref(readQuery('pageSize', defaultPageSize, s => Number(s) || defaultPageSize))
73
- const sort = ref<GscSortState | null>(readQuery('sort', defaultSort, parseSort))
74
- const filter = ref(defaultFilter) as Ref<TFilter>
75
-
76
- if (syncUrl) {
77
- watch(q, v => writeQuery('q', v || null))
78
- watch(page, v => writeQuery('page', v === defaultPage ? null : String(v)))
79
- watch(pageSize, v => writeQuery('pageSize', v === defaultPageSize ? null : String(v)))
80
- watch(sort, v => writeQuery('sort', serializeSort(v)))
81
- }
82
-
83
- // Reset page when q/filter changes (standard table UX).
84
- watch([q, filter], () => {
85
- page.value = defaultPage
86
- }, { deep: true })
87
-
88
- function reset(): void {
89
- q.value = defaultQ
90
- page.value = defaultPage
91
- pageSize.value = defaultPageSize
92
- sort.value = defaultSort
93
- filter.value = defaultFilter
94
- }
95
-
96
- function toggleSort(column: string): void {
97
- const cur = sort.value
98
- if (!cur || cur.column !== column) {
99
- sort.value = { column, direction: 'desc' }
100
- return
101
- }
102
- sort.value = cur.direction === 'desc' ? { column, direction: 'asc' } : null
103
- }
104
-
105
- return { q, page, pageSize, sort, filter, reset, toggleSort }
106
- }
107
-
108
- function serializeSort(s: GscSortState | null): string | null {
109
- return s ? `${s.column}:${s.direction}` : null
110
- }
111
-
112
- function parseSort(raw: string): GscSortState | null {
113
- if (!raw)
114
- return null
115
- const [column, direction] = raw.split(':')
116
- if (!column || (direction !== 'asc' && direction !== 'desc'))
117
- return null
118
- return { column, direction }
119
- }