Skip to content

Commit cf529c3

Browse files
committed
feat(prototype): query recipes, suggested queries, save dialog, type badges, URL state
- FilterOptions { excludeFunctions, excludeUnderscoreProps, excludeDollarProps } replace the old settings names; saved queries become source-agnostic recipes { query, title?, description?, ...FilterOptions } with title optional (untitled ids derive from a stable query hash) - data sources can provide suggested queries ({ queries?: Query[] }), listed read-only next to saved ones; loading any recipe applies its filter options - save query now opens an OverlayModal dialog (query preview, active filters, title/description, scope) - discovery struct annotations badge exotic values: Function, Map(n), Set(n), Date, class names, circular refs - source, query, and filters persist in the URL (shareable/restorable)
1 parent 36941c1 commit cf529c3

14 files changed

Lines changed: 348 additions & 137 deletions

File tree

examples/prototype-data-inspector/.devframe/data-inspector/queries.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"title": "Plugin names",
66
"description": "All Vite plugin names in order",
77
"query": "config.plugins.name",
8-
"sourceId": "vite:server",
98
"updatedAt": 1784177708176
109
}
1110
}

examples/prototype-data-inspector/README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,22 @@ against the Vite server serving the page itself — resizable split panes
2525
debounced auto-run with a manual re-run button, an empty query running `$`
2626
(the entire source object), client-side syntax gate, non-destructive errors,
2727
per-query stats (jora / normalize / rpc timings, payload size, node count),
28-
query settings (ignore functions / `_`-prefixed / `$`-prefixed, persisted,
29-
applied to results and skeleton alike), an "available data" panel showing the
30-
source's type skeleton independent of the query, remote autocomplete, saved
31-
queries in both scopes, and synced dark mode.
28+
filter options (exclude functions / `_` props / `$` props) applied to results
29+
and skeleton alike, an "available data" panel showing the source's type
30+
skeleton independent of the query, type badges on exotic values (`Function`,
31+
`Map(n)`, `Set(n)`, `Date`, class names, `circular`) via discovery's value
32+
annotations, remote autocomplete, source-provided suggested queries plus
33+
saved queries in both scopes (save dialog, recipes carry their filter
34+
options), the whole workbench state (source, query, filters) persisted in
35+
the URL, and synced dark mode.
3236

3337
## The pieces (what the real plugin reuses)
3438

3539
- **`src/registry.ts`** — data sources as
36-
`{ id, title, description?, getData(): any, static?: boolean }`, registered
37-
dynamically against the shared `DevframeNodeContext`
40+
`{ id, title, description?, getData(): any, static?: boolean, queries?: Query[] }`,
41+
registered dynamically against the shared `DevframeNodeContext`
3842
(`WeakMap` idiom from `plugins/git`). `static: true` memoizes `getData()`;
43+
`queries` are source-suggested recipes shown read-only in the UI;
3944
`registerDataSource` returns an unregister fn and fires change listeners.
4045
- **`src/normalize.ts`** — the core asset: live graph -> strict JSON
4146
(circular -> `$ref`, Map/Set/class/function/BigInt/Error tagging,
@@ -47,7 +52,9 @@ queries in both scopes, and synced dark mode.
4752
- **`src/query-engine.ts`** — jora with duck-typed `fromMap()` /
4853
`mapEntries()` / `fromSet()` / `ownKeys()` / `typeOf()` bridges, plus
4954
flattened stat-mode suggestions.
50-
- **`src/saved-queries.ts`** — id-keyed saved queries via devframe
55+
- **`src/saved-queries.ts`** — saved queries are source-agnostic recipes
56+
`{ query, title?, description?, ...FilterOptions }`, id-keyed (slug of the
57+
title, or a stable hash of the query when untitled) via devframe
5158
`createStorage` (debounced atomic JSON):
5259
`user` -> `node_modules/.data-inspector/queries.json` (per checkout),
5360
`project` -> `.devframe/data-inspector/queries.json` (committable, shared).

examples/prototype-data-inspector/src/normalize.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@ export interface NormalizeOptions {
2727
/** Max string length before truncation. */
2828
maxString?: number
2929
/** Drop function values (object props and array items). */
30-
ignoreFunctions?: boolean
30+
excludeFunctions?: boolean
3131
/** Drop object properties whose key starts with `_`. */
32-
ignoreUnderscorePrefixed?: boolean
32+
excludeUnderscoreProps?: boolean
3333
/** Drop object properties whose key starts with `$`. */
34-
ignoreDollarPrefixed?: boolean
34+
excludeDollarProps?: boolean
3535
}
3636

37-
/** True when a property key is excluded by the ignore settings. */
38-
export function isIgnoredKey(key: string, opts: Pick<NormalizeOptions, 'ignoreUnderscorePrefixed' | 'ignoreDollarPrefixed'>): boolean {
39-
if (opts.ignoreUnderscorePrefixed && key.startsWith('_'))
37+
/** True when a property key is excluded by the filter options. */
38+
export function isExcludedKey(key: string, opts: Pick<NormalizeOptions, 'excludeUnderscoreProps' | 'excludeDollarProps'>): boolean {
39+
if (opts.excludeUnderscoreProps && key.startsWith('_'))
4040
return true
41-
if (opts.ignoreDollarPrefixed && key.startsWith('$'))
41+
if (opts.excludeDollarProps && key.startsWith('$'))
4242
return true
4343
return false
4444
}
@@ -76,9 +76,9 @@ export function normalize(value: unknown, options: NormalizeOptions = {}): { dat
7676
maxEntries: options.maxEntries ?? 200,
7777
maxProps: options.maxProps ?? 150,
7878
maxString: options.maxString ?? 4000,
79-
ignoreFunctions: options.ignoreFunctions ?? false,
80-
ignoreUnderscorePrefixed: options.ignoreUnderscorePrefixed ?? false,
81-
ignoreDollarPrefixed: options.ignoreDollarPrefixed ?? false,
79+
excludeFunctions: options.excludeFunctions ?? false,
80+
excludeUnderscoreProps: options.excludeUnderscoreProps ?? false,
81+
excludeDollarProps: options.excludeDollarProps ?? false,
8282
},
8383
}
8484
const data = walk(value, walker, 0, '#')
@@ -146,7 +146,7 @@ function walk(value: unknown, w: Walker, depth: number, path: string): unknown {
146146
w.seen.set(obj, path)
147147

148148
if (Array.isArray(obj)) {
149-
const source = w.opts.ignoreFunctions ? obj.filter(item => typeof item !== 'function') : obj
149+
const source = w.opts.excludeFunctions ? obj.filter(item => typeof item !== 'function') : obj
150150
const cap = Math.min(source.length, w.opts.maxEntries)
151151
const out: unknown[] = Array.from({ length: cap })
152152
for (let i = 0; i < cap; i++)
@@ -201,7 +201,7 @@ function walk(value: unknown, w: Walker, depth: number, path: string): unknown {
201201
if (className && className !== 'Object')
202202
out.$class = className
203203

204-
const keys = Object.keys(obj).filter(key => !isIgnoredKey(key, w.opts))
204+
const keys = Object.keys(obj).filter(key => !isExcludedKey(key, w.opts))
205205
const cap = Math.min(keys.length, w.opts.maxProps)
206206
for (let i = 0; i < cap; i++) {
207207
const key = keys[i]
@@ -213,7 +213,7 @@ function walk(value: unknown, w: Walker, depth: number, path: string): unknown {
213213
out[key] = { $type: 'getter-error', message: error instanceof Error ? error.message : String(error) }
214214
continue
215215
}
216-
if (w.opts.ignoreFunctions && typeof v === 'function')
216+
if (w.opts.excludeFunctions && typeof v === 'function')
217217
continue
218218
out[key] = walk(v, w, depth + 1, `${path}.${key}`)
219219
}

examples/prototype-data-inspector/src/registry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* object, so anyone can register sources dynamically — at setup time or later —
88
* and the data-inspector finds them with no dependency edge beyond this module.
99
*/
10-
import type { DataSourceMeta } from './rpc-contract'
10+
import type { DataSourceMeta, Query } from './rpc-contract'
1111

1212
export interface DataSourceEntry {
1313
id: string
@@ -17,6 +17,8 @@ export interface DataSourceEntry {
1717
getData: () => unknown
1818
/** Data never changes; `getData()` is called once and memoized (default false). */
1919
static?: boolean
20+
/** Suggested queries, surfaced in the UI alongside saved ones (read-only). */
21+
queries?: Query[]
2022
}
2123

2224
interface Registry {
@@ -65,6 +67,7 @@ export function listDataSources(ctx: object): DataSourceMeta[] {
6567
title: entry.title,
6668
description: entry.description,
6769
static: entry.static ?? false,
70+
queries: entry.queries,
6871
}))
6972
}
7073

examples/prototype-data-inspector/src/rpc-contract.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,29 @@
55
* safe to import from browser code without dragging jora into the bundle.
66
*/
77

8+
/** Client-controlled filtering, applied by the normalizer and the skeleton. */
9+
export interface FilterOptions {
10+
excludeFunctions?: boolean
11+
excludeUnderscoreProps?: boolean
12+
excludeDollarProps?: boolean
13+
}
14+
15+
/** A query recipe: the text plus the filter options it was authored with. */
16+
export interface Query extends FilterOptions {
17+
query: string
18+
title?: string
19+
description?: string
20+
}
21+
822
/** What the client sees of a registered data source. */
923
export interface DataSourceMeta {
1024
id: string
1125
title: string
1226
description?: string
1327
/** Data never changes; the server memoizes `getData()`. */
1428
static: boolean
29+
/** Suggested queries provided by the source (shown read-only). */
30+
queries?: Query[]
1531
}
1632

1733
/** One completion candidate: replace [from, to) with `value`. */
@@ -51,37 +67,21 @@ export type QueryOutcome
5167
= | { ok: true, result: unknown, stats: QueryStats }
5268
| { ok: false, error: { name: string, message: string } }
5369

54-
/** Client-controlled query settings, forwarded to the normalizer/skeleton. */
55-
export interface QuerySettings {
56-
ignoreFunctions?: boolean
57-
ignoreUnderscorePrefixed?: boolean
58-
ignoreDollarPrefixed?: boolean
59-
}
60-
6170
export type SkeletonOutcome
6271
= | { ok: true, skeleton: unknown, nodes: number, ms: number }
6372
| { ok: false, error: { name: string, message: string } }
6473

6574
/** Where a saved query persists. */
6675
export type SavedQueryScope = 'user' | 'project'
6776

68-
export interface SavedQuery {
69-
/** Storage key. Derived from the title when not supplied. */
77+
export interface SavedQuery extends Query {
78+
/** Storage key. Derived from the title (or query) when not supplied. */
7079
id: string
71-
title: string
72-
description?: string
73-
query: string
74-
/** Source the query was authored against. */
75-
sourceId: string
7680
scope: SavedQueryScope
7781
updatedAt: number
7882
}
7983

80-
export interface SaveQueryInput {
84+
export interface SaveQueryInput extends Query {
8185
id?: string
82-
title: string
83-
description?: string
84-
query: string
85-
sourceId: string
8686
scope: SavedQueryScope
8787
}

examples/prototype-data-inspector/src/rpc-functions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* strict and proves wire-safety on every call.
88
*/
99
import type { DevframeNodeContext } from 'devframe/types'
10-
import type { QuerySettings, SavedQueryScope, SaveQueryInput, SkeletonOutcome } from './rpc-contract'
10+
import type { FilterOptions, SavedQueryScope, SaveQueryInput, SkeletonOutcome } from './rpc-contract'
1111
import { createDefineWrapperWithContext } from 'devframe/rpc'
1212
import { runQuery, suggest } from './query-engine'
1313
import { getDataSource, listDataSources, resolveSourceData } from './registry'
@@ -32,7 +32,7 @@ export const queryFn = defineRpc({
3232
type: 'query',
3333
jsonSerializable: true,
3434
setup: ctx => ({
35-
handler: async (sourceId: string, joraQuery: string, options?: { maxDepth?: number, maxEntries?: number } & QuerySettings) => {
35+
handler: async (sourceId: string, joraQuery: string, options?: { maxDepth?: number, maxEntries?: number } & FilterOptions) => {
3636
const source = getDataSource(ctx, sourceId)
3737
if (!source)
3838
return { ok: false as const, error: { name: 'UnknownSource', message: `No data source "${sourceId}"` } }
@@ -46,7 +46,7 @@ export const skeletonFn = defineRpc({
4646
type: 'query',
4747
jsonSerializable: true,
4848
setup: ctx => ({
49-
handler: async (sourceId: string, options?: QuerySettings): Promise<SkeletonOutcome> => {
49+
handler: async (sourceId: string, options?: FilterOptions): Promise<SkeletonOutcome> => {
5050
const source = getDataSource(ctx, sourceId)
5151
if (!source)
5252
return { ok: false, error: { name: 'UnknownSource', message: `No data source "${sourceId}"` } }

examples/prototype-data-inspector/src/saved-queries.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
* - `project` -> `<workspaceRoot>/.devframe/data-inspector/queries.json`
88
* (committable, shared with the team)
99
*
10-
* Backed by devframe's `createStorage` (debounced atomic JSON writes).
10+
* A saved query is a source-agnostic recipe: the query text, optional
11+
* title/description, and the FilterOptions it was authored with. Backed by
12+
* devframe's `createStorage` (debounced atomic JSON writes).
1113
*/
1214
import type { DevframeNodeContext } from 'devframe/types'
1315
import type { SavedQuery, SavedQueryScope, SaveQueryInput } from './rpc-contract'
@@ -45,12 +47,28 @@ function storesFor(ctx: DevframeNodeContext): Record<SavedQueryScope, Store> {
4547
return byScope
4648
}
4749

48-
function slugify(title: string): string {
49-
return title
50+
function slugify(text: string): string {
51+
return text
5052
.toLowerCase()
5153
.replace(/[^a-z0-9]+/g, '-')
5254
.replace(/^-+|-+$/g, '')
53-
|| `query-${Date.now()}`
55+
.slice(0, 60)
56+
}
57+
58+
/** djb2 — stable short id for untitled queries (same query, same id). */
59+
function hashOf(text: string): string {
60+
let hash = 5381
61+
for (let i = 0; i < text.length; i++)
62+
hash = ((hash << 5) + hash + text.charCodeAt(i)) >>> 0
63+
return hash.toString(36)
64+
}
65+
66+
function deriveId(input: SaveQueryInput): string {
67+
if (input.id)
68+
return input.id
69+
if (input.title?.trim())
70+
return slugify(input.title)
71+
return `q-${hashOf(input.query)}`
5472
}
5573

5674
export function listSavedQueries(ctx: DevframeNodeContext): SavedQuery[] {
@@ -65,13 +83,15 @@ export function listSavedQueries(ctx: DevframeNodeContext): SavedQuery[] {
6583

6684
export function saveQuery(ctx: DevframeNodeContext, input: SaveQueryInput): SavedQuery {
6785
const byScope = storesFor(ctx)
68-
const id = input.id ?? slugify(input.title)
86+
const id = deriveId(input)
6987
const record: Omit<SavedQuery, 'scope'> = {
7088
id,
71-
title: input.title,
72-
description: input.description || undefined,
7389
query: input.query,
74-
sourceId: input.sourceId,
90+
title: input.title?.trim() || undefined,
91+
description: input.description?.trim() || undefined,
92+
excludeFunctions: input.excludeFunctions || undefined,
93+
excludeUnderscoreProps: input.excludeUnderscoreProps || undefined,
94+
excludeDollarProps: input.excludeDollarProps || undefined,
7595
updatedAt: Date.now(),
7696
}
7797
byScope[input.scope].mutate((draft) => {

examples/prototype-data-inspector/src/skeleton.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
* - depth/prop caps -> '...'
1515
*/
1616
import type { NormalizeOptions } from './normalize'
17-
import { isIgnoredKey } from './normalize'
17+
import { isExcludedKey } from './normalize'
1818

1919
export type SkeletonOptions = Pick<
2020
NormalizeOptions,
21-
'maxDepth' | 'maxProps' | 'ignoreFunctions' | 'ignoreUnderscorePrefixed' | 'ignoreDollarPrefixed'
21+
'maxDepth' | 'maxProps' | 'excludeFunctions' | 'excludeUnderscoreProps' | 'excludeDollarProps'
2222
>
2323

2424
interface SkeletonWalker {
@@ -35,9 +35,9 @@ export function skeletonOf(value: unknown, options: SkeletonOptions = {}): { ske
3535
opts: {
3636
maxDepth: options.maxDepth ?? 5,
3737
maxProps: options.maxProps ?? 80,
38-
ignoreFunctions: options.ignoreFunctions ?? false,
39-
ignoreUnderscorePrefixed: options.ignoreUnderscorePrefixed ?? false,
40-
ignoreDollarPrefixed: options.ignoreDollarPrefixed ?? false,
38+
excludeFunctions: options.excludeFunctions ?? false,
39+
excludeUnderscoreProps: options.excludeUnderscoreProps ?? false,
40+
excludeDollarProps: options.excludeDollarProps ?? false,
4141
},
4242
}
4343
const skeleton = walk(value, walker, 0)
@@ -94,7 +94,7 @@ function walk(value: unknown, w: SkeletonWalker, depth: number): unknown {
9494

9595
function walkObject(obj: object, w: SkeletonWalker, depth: number): unknown {
9696
if (Array.isArray(obj)) {
97-
const items = w.opts.ignoreFunctions ? obj.filter(item => typeof item !== 'function') : obj
97+
const items = w.opts.excludeFunctions ? obj.filter(item => typeof item !== 'function') : obj
9898
if (items.length === 0)
9999
return []
100100
const first = walk(items[0], w, depth + 1)
@@ -130,7 +130,7 @@ function walkObject(obj: object, w: SkeletonWalker, depth: number): unknown {
130130
if (className && className !== 'Object')
131131
out.$class = className
132132

133-
const keys = Object.keys(obj).filter(key => !isIgnoredKey(key, w.opts))
133+
const keys = Object.keys(obj).filter(key => !isExcludedKey(key, w.opts))
134134
const cap = Math.min(keys.length, w.opts.maxProps)
135135
for (let i = 0; i < cap; i++) {
136136
const key = keys[i]
@@ -142,7 +142,7 @@ function walkObject(obj: object, w: SkeletonWalker, depth: number): unknown {
142142
out[key] = 'getter-error'
143143
continue
144144
}
145-
if (w.opts.ignoreFunctions && typeof v === 'function')
145+
if (w.opts.excludeFunctions && typeof v === 'function')
146146
continue
147147
out[key] = walk(v, w, depth + 1)
148148
}

0 commit comments

Comments
 (0)