fastapi-voyager 0.15.2__py3-none-any.whl → 0.15.3__py3-none-any.whl

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.
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.15.2"
2
+ __version__ = "0.15.3"
@@ -1,10 +1,6 @@
1
1
  const { reactive } = window.Vue
2
2
 
3
3
  const state = reactive({
4
- item: {
5
- count: 0,
6
- },
7
-
8
4
  version: "",
9
5
  config: {
10
6
  initial_page_policy: "first",
@@ -15,7 +11,8 @@ const state = reactive({
15
11
  mode: "voyager", // voyager / er-diagram
16
12
 
17
13
  previousTagRoute: {
18
- // for shift + click, store previous tag/route, and populate back when needed
14
+ // Store the last non-search tag/route selection for restoration when clearing search
15
+ // Used by resetSearch to return to the state before entering search mode
19
16
  hasValue: false,
20
17
  tag: null,
21
18
  routeId: null,
@@ -65,6 +62,9 @@ const state = reactive({
65
62
  fieldOptions: [],
66
63
  },
67
64
 
65
+ // cache all schema options for filtering
66
+ allSchemaOptions: [],
67
+
68
68
  // route information
69
69
  routeDetail: {
70
70
  show: false,
@@ -105,13 +105,425 @@ const state = reactive({
105
105
  },
106
106
  })
107
107
 
108
- const mutations = {
109
- increment() {
110
- state.item.count += 1
108
+ const getters = {
109
+ /**
110
+ * Find tag name by route ID
111
+ * Used to determine which tag a route belongs to
112
+ */
113
+ findTagByRoute(routeId) {
114
+ return (
115
+ state.leftPanel.tags.find((tag) => (tag.routes || []).some((route) => route.id === routeId))
116
+ ?.name || null
117
+ )
118
+ },
119
+ }
120
+
121
+ const actions = {
122
+ /**
123
+ * Read tag and route from URL query parameters
124
+ * @returns {{ tag: string|null, route: string|null }}
125
+ */
126
+ readQuerySelection() {
127
+ if (typeof window === "undefined") {
128
+ return { tag: null, route: null }
129
+ }
130
+ const params = new URLSearchParams(window.location.search)
131
+ return {
132
+ tag: params.get("tag") || null,
133
+ route: params.get("route") || null,
134
+ }
135
+ },
136
+
137
+ /**
138
+ * Sync current tag and route selection to URL
139
+ * Updates browser URL without reloading the page
140
+ */
141
+ syncSelectionToUrl() {
142
+ if (typeof window === "undefined") {
143
+ return
144
+ }
145
+ const params = new URLSearchParams(window.location.search)
146
+ if (state.leftPanel.tag) {
147
+ params.set("tag", state.leftPanel.tag)
148
+ } else {
149
+ params.delete("tag")
150
+ }
151
+ if (state.leftPanel.routeId) {
152
+ params.set("route", state.leftPanel.routeId)
153
+ } else {
154
+ params.delete("route")
155
+ }
156
+ const hash = window.location.hash || ""
157
+ const search = params.toString()
158
+ const base = window.location.pathname
159
+ const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`
160
+ window.history.replaceState({}, "", newUrl)
161
+ },
162
+
163
+ /**
164
+ * Apply selection from URL query parameters to state
165
+ * @param {{ tag: string|null, route: string|null }} selection
166
+ * @returns {boolean} - true if any selection was applied
167
+ */
168
+ applySelectionFromQuery(selection) {
169
+ let applied = false
170
+ if (selection.tag && state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
171
+ state.leftPanel.tag = selection.tag
172
+ state.leftPanel._tag = selection.tag
173
+ applied = true
174
+ }
175
+ if (selection.route && state.graph.routeItems?.[selection.route]) {
176
+ state.leftPanel.routeId = selection.route
177
+ applied = true
178
+ const inferredTag = getters.findTagByRoute(selection.route)
179
+ if (inferredTag) {
180
+ state.leftPanel.tag = inferredTag
181
+ state.leftPanel._tag = inferredTag
182
+ }
183
+ }
184
+ return applied
185
+ },
186
+
187
+ /**
188
+ * Restore full tags from cache
189
+ * Used when resetting search mode
190
+ */
191
+ loadFullTags() {
192
+ state.leftPanel.tags = state.leftPanel.fullTagsCache
193
+ },
194
+
195
+ /**
196
+ * Populate field options based on selected schema
197
+ * @param {string} schemaId - Schema ID
198
+ */
199
+ populateFieldOptions(schemaId) {
200
+ if (!schemaId) {
201
+ state.search.fieldOptions = []
202
+ state.search.fieldName = null
203
+ return
204
+ }
205
+ const schema = state.graph.schemaMap?.[schemaId]
206
+ if (!schema) {
207
+ state.search.fieldOptions = []
208
+ state.search.fieldName = null
209
+ return
210
+ }
211
+ const fields = Array.isArray(schema.fields) ? schema.fields.map((f) => f.name) : []
212
+ state.search.fieldOptions = fields
213
+ if (!fields.includes(state.search.fieldName)) {
214
+ state.search.fieldName = null
215
+ }
216
+ },
217
+
218
+ /**
219
+ * Rebuild schema options from schema map
220
+ * Should be called when schema map changes
221
+ */
222
+ rebuildSchemaOptions() {
223
+ const dict = state.graph.schemaMap || {}
224
+ const opts = Object.values(dict).map((s) => ({
225
+ label: s.name,
226
+ desc: s.id,
227
+ value: s.id,
228
+ }))
229
+ state.allSchemaOptions = opts
230
+ state.search.schemaOptions = opts.slice()
231
+ this.populateFieldOptions(state.search.schemaName)
232
+ },
233
+
234
+ /**
235
+ * Load tags based on search criteria
236
+ * @returns {Promise<void>}
237
+ */
238
+ async loadSearchedTags() {
239
+ try {
240
+ const payload = {
241
+ schema_name: state.search.schemaName,
242
+ schema_field: state.search.fieldName || null,
243
+ show_fields: state.filter.showFields,
244
+ brief: state.filter.brief,
245
+ hide_primitive_route: state.filter.hidePrimitiveRoute,
246
+ show_module: state.filter.showModule,
247
+ }
248
+ const res = await fetch("dot-search", {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify(payload),
252
+ })
253
+ if (res.ok) {
254
+ const data = await res.json()
255
+ const tags = Array.isArray(data.tags) ? data.tags : []
256
+ state.leftPanel.tags = tags
257
+ }
258
+ } catch (err) {
259
+ console.error("dot-search failed", err)
260
+ }
261
+ },
262
+
263
+ /**
264
+ * Load initial data from API
265
+ * @param {Function} onGenerate - Callback to generate graph after load
266
+ * @param {Function} renderBasedOnInitialPolicy - Callback to render based on policy
267
+ * @returns {Promise<void>}
268
+ */
269
+ async loadInitial(onGenerate, renderBasedOnInitialPolicy) {
270
+ state.initializing = true
271
+ try {
272
+ const res = await fetch("dot")
273
+ const data = await res.json()
274
+ const tags = Array.isArray(data.tags) ? data.tags : []
275
+ state.leftPanel.tags = tags
276
+ // Cache the full tags for later use (e.g., resetSearch)
277
+ state.leftPanel.fullTagsCache = tags
278
+
279
+ const schemasArr = Array.isArray(data.schemas) ? data.schemas : []
280
+ // Build dict keyed by id for faster lookups and simpler prop passing
281
+ const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]))
282
+ state.graph.schemaMap = schemaMap
283
+ state.graph.schemaKeys = new Set(Object.keys(schemaMap))
284
+ state.graph.routeItems = data.tags
285
+ .map((t) => t.routes)
286
+ .flat()
287
+ .reduce((acc, r) => {
288
+ acc[r.id] = r
289
+ return acc
290
+ }, {})
291
+ state.modeControl.briefModeEnabled = data.enable_brief_mode || false
292
+ state.version = data.version || ""
293
+ state.swagger.url = data.swagger_url || null
294
+ state.config.has_er_diagram = data.has_er_diagram || false
295
+ state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
296
+
297
+ this.rebuildSchemaOptions()
298
+
299
+ const querySelection = this.readQuerySelection()
300
+ const restoredFromQuery = this.applySelectionFromQuery(querySelection)
301
+ if (restoredFromQuery) {
302
+ this.syncSelectionToUrl()
303
+ onGenerate()
304
+ return
305
+ } else {
306
+ state.config.initial_page_policy = data.initial_page_policy
307
+ renderBasedOnInitialPolicy()
308
+ }
309
+
310
+ // default route options placeholder
311
+ } catch (e) {
312
+ console.error("Initial load failed", e)
313
+ } finally {
314
+ state.initializing = false
315
+ }
316
+ },
317
+
318
+ /**
319
+ * Filter schema options based on search text
320
+ * Used by Quasar select component's filter function
321
+ * @param {string} val - Search text
322
+ * @param {Function} update - Quasar update callback
323
+ */
324
+ filterSearchSchemas(val, update) {
325
+ const needle = (val || "").toLowerCase()
326
+ update(() => {
327
+ if (!needle) {
328
+ state.search.schemaOptions = state.allSchemaOptions.slice()
329
+ return
330
+ }
331
+ state.search.schemaOptions = state.allSchemaOptions.filter((option) =>
332
+ option.label.toLowerCase().includes(needle)
333
+ )
334
+ })
335
+ },
336
+
337
+ /**
338
+ * Handle schema selection change
339
+ * Updates state and triggers search if a schema is selected
340
+ * @param {string} val - Selected schema ID
341
+ * @param {Function} onSearch - Callback to trigger search
342
+ */
343
+ onSearchSchemaChange(val, onSearch) {
344
+ state.search.schemaName = val
345
+ state.search.mode = false
346
+ if (!val) {
347
+ // Clearing the select should only run resetSearch via @clear
348
+ return
349
+ }
350
+ onSearch()
351
+ },
352
+
353
+ /**
354
+ * Reset detail panels (right drawer and route detail)
355
+ */
356
+ resetDetailPanels() {
357
+ state.rightDrawer.drawer = false
358
+ state.routeDetail.show = false
359
+ state.schemaDetail.schemaCodeName = ""
360
+ },
361
+
362
+ /**
363
+ * Reset left panel selection and regenerate
364
+ * @param {Function} onGenerate - Callback to regenerate graph
365
+ */
366
+ onReset(onGenerate) {
367
+ state.leftPanel.tag = null
368
+ state.leftPanel._tag = null
369
+ state.leftPanel.routeId = null
370
+ this.syncSelectionToUrl()
371
+ onGenerate()
372
+ },
373
+
374
+ /**
375
+ * Toggle pydantic resolve meta visibility
376
+ * @param {boolean} val - New value
377
+ * @param {Function} onGenerate - Callback to regenerate graph
378
+ */
379
+ togglePydanticResolveMeta(val, onGenerate) {
380
+ state.modeControl.pydanticResolveMetaEnabled = val
381
+ try {
382
+ localStorage.setItem("pydantic_resolve_meta", JSON.stringify(val))
383
+ } catch (e) {
384
+ console.warn("Failed to save pydantic_resolve_meta to localStorage", e)
385
+ }
386
+ onGenerate()
387
+ },
388
+
389
+ /**
390
+ * Toggle show module clustering
391
+ * @param {boolean} val - New value
392
+ * @param {Function} onGenerate - Callback to regenerate graph
393
+ */
394
+ toggleShowModule(val, onGenerate) {
395
+ state.filter.showModule = val
396
+ try {
397
+ localStorage.setItem("show_module_cluster", JSON.stringify(val))
398
+ } catch (e) {
399
+ console.warn("Failed to save show_module_cluster to localStorage", e)
400
+ }
401
+ onGenerate()
402
+ },
403
+
404
+ /**
405
+ * Toggle show fields option
406
+ * @param {string} field - Field display option ("single", "object", "all")
407
+ * @param {Function} onGenerate - Callback to regenerate graph
408
+ */
409
+ toggleShowField(field, onGenerate) {
410
+ state.filter.showFields = field
411
+ onGenerate(false)
412
+ },
413
+
414
+ /**
415
+ * Toggle brief mode
416
+ * @param {boolean} val - New value
417
+ * @param {Function} onGenerate - Callback to regenerate graph
418
+ */
419
+ toggleBrief(val, onGenerate) {
420
+ state.filter.brief = val
421
+ try {
422
+ localStorage.setItem("brief_mode", JSON.stringify(val))
423
+ } catch (e) {
424
+ console.warn("Failed to save brief_mode to localStorage", e)
425
+ }
426
+ onGenerate()
427
+ },
428
+
429
+ /**
430
+ * Toggle hide primitive route
431
+ * @param {boolean} val - New value
432
+ * @param {Function} onGenerate - Callback to regenerate graph
433
+ */
434
+ toggleHidePrimitiveRoute(val, onGenerate) {
435
+ state.filter.hidePrimitiveRoute = val
436
+ try {
437
+ localStorage.setItem("hide_primitive", JSON.stringify(val))
438
+ } catch (e) {
439
+ console.warn("Failed to save hide_primitive to localStorage", e)
440
+ }
441
+ onGenerate(false)
442
+ },
443
+
444
+ /**
445
+ * Render based on initial page policy
446
+ * @param {Function} onGenerate - Callback to regenerate graph
447
+ */
448
+ renderBasedOnInitialPolicy(onGenerate) {
449
+ switch (state.config.initial_page_policy) {
450
+ case "full":
451
+ onGenerate()
452
+ return
453
+ case "empty":
454
+ return
455
+ case "first":
456
+ state.leftPanel.tag = state.leftPanel.tags.length > 0 ? state.leftPanel.tags[0].name : null
457
+ state.leftPanel._tag = state.leftPanel.tag
458
+ this.syncSelectionToUrl()
459
+ onGenerate()
460
+ return
461
+ }
462
+ },
463
+
464
+ /**
465
+ * Build payload for Voyager rendering
466
+ * @returns {Object} Payload for dot API
467
+ */
468
+ buildVoyagerPayload() {
469
+ const activeSchema = state.search.mode ? state.search.schemaName : null
470
+ const activeField = state.search.mode ? state.search.fieldName : null
471
+ return {
472
+ tags: state.leftPanel.tag ? [state.leftPanel.tag] : null,
473
+ schema_name: activeSchema || null,
474
+ schema_field: activeField || null,
475
+ route_name: state.leftPanel.routeId || null,
476
+ show_fields: state.filter.showFields,
477
+ brief: state.filter.brief,
478
+ hide_primitive_route: state.filter.hidePrimitiveRoute,
479
+ show_module: state.filter.showModule,
480
+ show_pydantic_resolve_meta: state.modeControl.pydanticResolveMetaEnabled,
481
+ }
482
+ },
483
+
484
+ /**
485
+ * Build payload for ER Diagram rendering
486
+ * @returns {Object} Payload for er-diagram API
487
+ */
488
+ buildErDiagramPayload() {
489
+ return {
490
+ show_fields: state.filter.showFields,
491
+ show_module: state.filter.showModule,
492
+ }
493
+ },
494
+
495
+ /**
496
+ * Restore search state and return whether to regenerate
497
+ * @returns {boolean} - true if should regenerate with previous selection
498
+ */
499
+ resetSearchState() {
500
+ state.search.mode = false
501
+ const hadPreviousValue = state.previousTagRoute.hasValue
502
+
503
+ if (hadPreviousValue) {
504
+ state.leftPanel.tag = state.previousTagRoute.tag
505
+ state.leftPanel._tag = state.previousTagRoute.tag
506
+ state.leftPanel.routeId = state.previousTagRoute.routeId
507
+ // Clear the saved state
508
+ state.previousTagRoute.hasValue = false
509
+ } else {
510
+ state.leftPanel.tag = null
511
+ state.leftPanel._tag = null
512
+ state.leftPanel.routeId = null
513
+ }
514
+
515
+ this.syncSelectionToUrl()
516
+ this.loadFullTags()
517
+
518
+ return hadPreviousValue
111
519
  },
112
520
  }
113
521
 
522
+ const mutations = {}
523
+
114
524
  export const store = {
115
525
  state,
526
+ getters,
527
+ actions,
116
528
  mutations,
117
529
  }
@@ -22,7 +22,6 @@ function loadToggleState(key, defaultValue = false) {
22
22
  const app = createApp({
23
23
  setup() {
24
24
  let graphUI = null
25
- const allSchemaOptions = ref([])
26
25
  const erDiagramLoading = ref(false)
27
26
  const erDiagramCache = ref("")
28
27
 
@@ -42,20 +41,13 @@ const app = createApp({
42
41
  graphUI = new GraphUI("#graph", {
43
42
  onSchemaShiftClick: (id) => {
44
43
  if (store.state.graph.schemaKeys.has(id)) {
45
- // Only save current tag/route if we're not already in search mode
46
- // This prevents overwriting the saved state with null values
47
- if (!store.state.previousTagRoute.hasValue && !store.state.search.mode) {
48
- store.state.previousTagRoute.tag = store.state.leftPanel.tag
49
- store.state.previousTagRoute.routeId = store.state.leftPanel.routeId
50
- store.state.previousTagRoute.hasValue = true
51
- }
52
44
  store.state.search.mode = true
53
45
  store.state.search.schemaName = id
54
46
  onSearch()
55
47
  }
56
48
  },
57
49
  onSchemaClick: (id) => {
58
- resetDetailPanels()
50
+ store.actions.resetDetailPanels()
59
51
  if (store.state.graph.schemaKeys.has(id)) {
60
52
  store.state.schemaDetail.schemaCodeName = id
61
53
  store.state.rightDrawer.drawer = true
@@ -66,257 +58,44 @@ const app = createApp({
66
58
  }
67
59
  },
68
60
  resetCb: () => {
69
- resetDetailPanels()
61
+ store.actions.resetDetailPanels()
70
62
  },
71
63
  })
72
64
  }
73
65
 
74
- function rebuildSchemaOptions() {
75
- const dict = store.state.graph.schemaMap || {}
76
- const opts = Object.values(dict).map((s) => ({
77
- label: s.name,
78
- desc: s.id,
79
- value: s.id,
80
- }))
81
- allSchemaOptions.value = opts
82
- store.state.search.schemaOptions = opts.slice()
83
- populateFieldOptions(store.state.search.schemaName)
84
- }
85
-
86
- function populateFieldOptions(schemaId) {
87
- if (!schemaId) {
88
- store.state.search.fieldOptions = []
89
- store.state.search.fieldName = null
90
- return
91
- }
92
- const schema = store.state.graph.schemaMap?.[schemaId]
93
- if (!schema) {
94
- store.state.search.fieldOptions = []
95
- store.state.search.fieldName = null
96
- return
97
- }
98
- const fields = Array.isArray(schema.fields) ? schema.fields.map((f) => f.name) : []
99
- store.state.search.fieldOptions = fields
100
- if (!fields.includes(store.state.search.fieldName)) {
101
- store.state.search.fieldName = null
102
- }
103
- }
104
-
105
- function filterSearchSchemas(val, update) {
106
- const needle = (val || "").toLowerCase()
107
- update(() => {
108
- if (!needle) {
109
- store.state.search.schemaOptions = allSchemaOptions.value.slice()
110
- return
111
- }
112
- store.state.search.schemaOptions = allSchemaOptions.value.filter((option) =>
113
- option.label.toLowerCase().includes(needle)
114
- )
115
- })
116
- }
117
-
118
- function onSearchSchemaChange(val) {
119
- store.state.search.schemaName = val
120
- store.state.search.mode = false
121
- if (!val) {
122
- // Clearing the select should only run resetSearch via @clear
123
- return
124
- }
125
- onSearch()
126
- }
127
-
128
- function readQuerySelection() {
129
- if (typeof window === "undefined") {
130
- return { tag: null, route: null }
131
- }
132
- const params = new URLSearchParams(window.location.search)
133
- return {
134
- tag: params.get("tag") || null,
135
- route: params.get("route") || null,
136
- }
137
- }
138
-
139
- function findTagByRoute(routeId) {
140
- return (
141
- store.state.leftPanel.tags.find((tag) =>
142
- (tag.routes || []).some((route) => route.id === routeId)
143
- )?.name || null
144
- )
145
- }
146
-
147
- function syncSelectionToUrl() {
148
- if (typeof window === "undefined") {
149
- return
150
- }
151
- const params = new URLSearchParams(window.location.search)
152
- if (store.state.leftPanel.tag) {
153
- params.set("tag", store.state.leftPanel.tag)
154
- } else {
155
- params.delete("tag")
156
- }
157
- if (store.state.leftPanel.routeId) {
158
- params.set("route", store.state.leftPanel.routeId)
159
- } else {
160
- params.delete("route")
161
- }
162
- const hash = window.location.hash || ""
163
- const search = params.toString()
164
- const base = window.location.pathname
165
- const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`
166
- window.history.replaceState({}, "", newUrl)
167
- }
168
-
169
- function applySelectionFromQuery(selection) {
170
- let applied = false
171
- if (selection.tag && store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
172
- store.state.leftPanel.tag = selection.tag
173
- store.state.leftPanel._tag = selection.tag
174
- applied = true
175
- }
176
- if (selection.route && store.state.graph.routeItems?.[selection.route]) {
177
- store.state.leftPanel.routeId = selection.route
178
- applied = true
179
- const inferredTag = findTagByRoute(selection.route)
180
- if (inferredTag) {
181
- store.state.leftPanel.tag = inferredTag
182
- store.state.leftPanel._tag = inferredTag
183
- }
184
- }
185
- return applied
186
- }
187
-
188
66
  async function resetSearch() {
189
- store.state.search.mode = false
190
- const hadPreviousValue = store.state.previousTagRoute.hasValue
191
-
192
- if (hadPreviousValue) {
193
- store.state.leftPanel.tag = store.state.previousTagRoute.tag
194
- store.state.leftPanel._tag = store.state.previousTagRoute.tag
195
- store.state.leftPanel.routeId = store.state.previousTagRoute.routeId
196
- store.state.previousTagRoute.hasValue = false
197
- } else {
198
- store.state.leftPanel.tag = null
199
- store.state.leftPanel._tag = null
200
- store.state.leftPanel.routeId = null
201
- }
202
-
203
- syncSelectionToUrl()
204
-
205
- // Load the full tags from cache (not search results) since we're resetting search
206
- loadFullTags()
67
+ const hadPreviousValue = store.actions.resetSearchState()
207
68
 
208
69
  // If we restored a previous tag/route, generate with it
209
70
  // Otherwise, fall back to initial policy
210
71
  if (hadPreviousValue) {
211
72
  onGenerate()
212
73
  } else {
213
- renderBasedOnInitialPolicy()
74
+ store.actions.renderBasedOnInitialPolicy(onGenerate)
214
75
  }
215
76
  }
216
77
 
217
- function loadFullTags() {
218
- // Restore from cache (set by loadInitial)
219
- store.state.leftPanel.tags = store.state.leftPanel.fullTagsCache
220
- }
221
-
222
78
  async function onSearch() {
79
+ // Save current state before entering search mode (only if not already saved)
80
+ if (!store.state.previousTagRoute.hasValue) {
81
+ store.state.previousTagRoute.tag = store.state.leftPanel.tag
82
+ store.state.previousTagRoute.routeId = store.state.leftPanel.routeId
83
+ store.state.previousTagRoute.hasValue = true
84
+ }
85
+
223
86
  store.state.search.mode = true
224
87
  store.state.leftPanel.tag = null
225
88
  store.state.leftPanel._tag = null
226
89
  store.state.leftPanel.routeId = null
227
- syncSelectionToUrl()
228
- await loadSearchedTags()
90
+ store.actions.syncSelectionToUrl()
91
+ await store.actions.loadSearchedTags()
229
92
  await onGenerate()
230
93
  }
231
- async function loadSearchedTags() {
232
- try {
233
- const payload = {
234
- schema_name: store.state.search.schemaName,
235
- schema_field: store.state.search.fieldName || null,
236
- show_fields: store.state.filter.showFields,
237
- brief: store.state.filter.brief,
238
- hide_primitive_route: store.state.filter.hidePrimitiveRoute,
239
- show_module: store.state.filter.showModule,
240
- }
241
- const res = await fetch("dot-search", {
242
- method: "POST",
243
- headers: { "Content-Type": "application/json" },
244
- body: JSON.stringify(payload),
245
- })
246
- if (res.ok) {
247
- const data = await res.json()
248
- const tags = Array.isArray(data.tags) ? data.tags : []
249
- store.state.leftPanel.tags = tags
250
- }
251
- } catch (err) {
252
- console.error("dot-search failed", err)
253
- }
254
- }
255
94
 
256
95
  async function loadInitial() {
257
- store.state.initializing = true
258
- try {
259
- const res = await fetch("dot")
260
- const data = await res.json()
261
- const tags = Array.isArray(data.tags) ? data.tags : []
262
- store.state.leftPanel.tags = tags
263
- // Cache the full tags for later use (e.g., resetSearch)
264
- store.state.leftPanel.fullTagsCache = tags
265
-
266
- const schemasArr = Array.isArray(data.schemas) ? data.schemas : []
267
- // Build dict keyed by id for faster lookups and simpler prop passing
268
- const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]))
269
- store.state.graph.schemaMap = schemaMap
270
- store.state.graph.schemaKeys = new Set(Object.keys(schemaMap))
271
- store.state.graph.routeItems = data.tags
272
- .map((t) => t.routes)
273
- .flat()
274
- .reduce((acc, r) => {
275
- acc[r.id] = r
276
- return acc
277
- }, {})
278
- store.state.modeControl.briefModeEnabled = data.enable_brief_mode || false
279
- store.state.version = data.version || ""
280
- store.state.swagger.url = data.swagger_url || null
281
- store.state.config.has_er_diagram = data.has_er_diagram || false
282
- store.state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
283
-
284
- rebuildSchemaOptions()
285
-
286
- const querySelection = readQuerySelection()
287
- const restoredFromQuery = applySelectionFromQuery(querySelection)
288
- if (restoredFromQuery) {
289
- syncSelectionToUrl()
290
- onGenerate()
291
- return
292
- } else {
293
- store.state.config.initial_page_policy = data.initial_page_policy
294
- renderBasedOnInitialPolicy()
295
- }
296
-
297
- // default route options placeholder
298
- } catch (e) {
299
- console.error("Initial load failed", e)
300
- } finally {
301
- store.state.initializing = false
302
- }
303
- }
304
-
305
- async function renderBasedOnInitialPolicy() {
306
- switch (store.state.config.initial_page_policy) {
307
- case "full":
308
- onGenerate()
309
- return
310
- case "empty":
311
- return
312
- case "first":
313
- store.state.leftPanel.tag =
314
- store.state.leftPanel.tags.length > 0 ? store.state.leftPanel.tags[0].name : null
315
- store.state.leftPanel._tag = store.state.leftPanel.tag
316
- syncSelectionToUrl()
317
- onGenerate()
318
- return
319
- }
96
+ await store.actions.loadInitial(onGenerate, (cb) =>
97
+ store.actions.renderBasedOnInitialPolicy(cb)
98
+ )
320
99
  }
321
100
 
322
101
  async function onGenerate(resetZoom = true) {
@@ -331,21 +110,9 @@ const app = createApp({
331
110
  }
332
111
 
333
112
  async function renderVoyager(resetZoom = true) {
334
- const activeSchema = store.state.search.mode ? store.state.search.schemaName : null
335
- const activeField = store.state.search.mode ? store.state.search.fieldName : null
336
113
  store.state.generating = true
337
114
  try {
338
- const payload = {
339
- tags: store.state.leftPanel.tag ? [store.state.leftPanel.tag] : null,
340
- schema_name: activeSchema || null,
341
- schema_field: activeField || null,
342
- route_name: store.state.leftPanel.routeId || null,
343
- show_fields: store.state.filter.showFields,
344
- brief: store.state.filter.brief,
345
- hide_primitive_route: store.state.filter.hidePrimitiveRoute,
346
- show_module: store.state.filter.showModule,
347
- show_pydantic_resolve_meta: store.state.modeControl.pydanticResolveMetaEnabled,
348
- }
115
+ const payload = store.actions.buildVoyagerPayload()
349
116
  initGraphUI()
350
117
  const res = await fetch("dot", {
351
118
  method: "POST",
@@ -362,37 +129,10 @@ const app = createApp({
362
129
  }
363
130
  }
364
131
 
365
- function resetDetailPanels() {
366
- store.state.rightDrawer.drawer = false
367
- store.state.routeDetail.show = false
368
- store.state.schemaDetail.schemaCodeName = ""
369
- }
370
-
371
- async function onReset() {
372
- store.state.leftPanel.tag = null
373
- store.state.leftPanel._tag = null
374
- store.state.leftPanel.routeId = null
375
- syncSelectionToUrl()
376
- onGenerate()
377
- }
378
-
379
- async function togglePydanticResolveMeta(val) {
380
- store.state.modeControl.pydanticResolveMetaEnabled = val
381
- try {
382
- localStorage.setItem("pydantic_resolve_meta", JSON.stringify(val))
383
- } catch (e) {
384
- console.warn("Failed to save pydantic_resolve_meta to localStorage", e)
385
- }
386
- onGenerate()
387
- }
388
-
389
132
  async function renderErDiagram(resetZoom = true) {
390
133
  initGraphUI()
391
134
  erDiagramLoading.value = true
392
- const payload = {
393
- show_fields: store.state.filter.showFields,
394
- show_module: store.state.filter.showModule,
395
- }
135
+ const payload = store.actions.buildErDiagramPayload()
396
136
  try {
397
137
  const res = await fetch("er-diagram", {
398
138
  method: "POST",
@@ -447,7 +187,7 @@ const app = createApp({
447
187
 
448
188
  store.state.rightDrawer.drawer = false
449
189
  store.state.routeDetail.show = false
450
- syncSelectionToUrl()
190
+ store.actions.syncSelectionToUrl()
451
191
  }
452
192
 
453
193
  function toggleTagNavigatorCollapse() {
@@ -468,7 +208,7 @@ const app = createApp({
468
208
 
469
209
  function selectRoute(routeId) {
470
210
  // find belonging tag
471
- const belongingTag = findTagByRoute(routeId)
211
+ const belongingTag = store.getters.findTagByRoute(routeId)
472
212
  if (belongingTag) {
473
213
  store.state.leftPanel.tag = belongingTag
474
214
  store.state.leftPanel._tag = belongingTag
@@ -483,45 +223,10 @@ const app = createApp({
483
223
  store.state.rightDrawer.drawer = false
484
224
  store.state.routeDetail.show = false
485
225
  store.state.schemaDetail.schemaCodeName = ""
486
- syncSelectionToUrl()
487
- onGenerate()
488
- }
489
-
490
- function toggleShowModule(val) {
491
- store.state.filter.showModule = val
492
- try {
493
- localStorage.setItem("show_module_cluster", JSON.stringify(val))
494
- } catch (e) {
495
- console.warn("Failed to save show_module_cluster to localStorage", e)
496
- }
226
+ store.actions.syncSelectionToUrl()
497
227
  onGenerate()
498
228
  }
499
229
 
500
- function toggleShowField(field) {
501
- store.state.filter.showFields = field
502
- onGenerate(false)
503
- }
504
-
505
- function toggleBrief(val) {
506
- store.state.filter.brief = val
507
- try {
508
- localStorage.setItem("brief_mode", JSON.stringify(val))
509
- } catch (e) {
510
- console.warn("Failed to save brief_mode to localStorage", e)
511
- }
512
- onGenerate()
513
- }
514
-
515
- function toggleHidePrimitiveRoute(val) {
516
- store.state.filter.hidePrimitiveRoute = val
517
- try {
518
- localStorage.setItem("hide_primitive", JSON.stringify(val))
519
- } catch (e) {
520
- console.warn("Failed to save hide_primitive to localStorage", e)
521
- }
522
- onGenerate(false)
523
- }
524
-
525
230
  function startDragDrawer(e) {
526
231
  const startX = e.clientX
527
232
  const startWidth = store.state.rightDrawer.width
@@ -549,7 +254,7 @@ const app = createApp({
549
254
  watch(
550
255
  () => store.state.graph.schemaMap,
551
256
  () => {
552
- rebuildSchemaOptions()
257
+ store.actions.rebuildSchemaOptions()
553
258
  },
554
259
  { deep: false }
555
260
  )
@@ -573,8 +278,8 @@ const app = createApp({
573
278
  watch(
574
279
  () => store.state.search.schemaName,
575
280
  (schemaId) => {
576
- store.state.search.schemaOptions = allSchemaOptions.value.slice()
577
- populateFieldOptions(schemaId)
281
+ store.state.search.schemaOptions = store.state.allSchemaOptions.slice()
282
+ store.actions.populateFieldOptions(schemaId)
578
283
  if (!schemaId) {
579
284
  store.state.search.mode = false
580
285
  }
@@ -591,21 +296,22 @@ const app = createApp({
591
296
  store,
592
297
  onSearch,
593
298
  resetSearch,
594
- filterSearchSchemas,
595
- onSearchSchemaChange,
299
+ filterSearchSchemas: (val, update) => store.actions.filterSearchSchemas(val, update),
300
+ onSearchSchemaChange: (val) => store.actions.onSearchSchemaChange(val, onSearch),
596
301
  toggleTag,
597
302
  toggleTagNavigatorCollapse,
598
- toggleBrief,
599
- toggleHidePrimitiveRoute,
303
+ toggleBrief: (val) => store.actions.toggleBrief(val, onGenerate),
304
+ toggleHidePrimitiveRoute: (val) => store.actions.toggleHidePrimitiveRoute(val, onGenerate),
600
305
  selectRoute,
601
306
  onGenerate,
602
- onReset,
603
- toggleShowField,
307
+ onReset: () => store.actions.onReset(onGenerate),
308
+ toggleShowField: (field) => store.actions.toggleShowField(field, onGenerate),
604
309
  startDragDrawer,
605
- toggleShowModule,
310
+ toggleShowModule: (val) => store.actions.toggleShowModule(val, onGenerate),
606
311
  onModeChange,
607
312
  renderErDiagram,
608
- togglePydanticResolveMeta,
313
+ togglePydanticResolveMeta: (val) => store.actions.togglePydanticResolveMeta(val, onGenerate),
314
+ resetDetailPanels: () => store.actions.resetDetailPanels(),
609
315
  }
610
316
  },
611
317
  })
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.15.2
3
+ Version: 0.15.3
4
4
  Summary: Visualize FastAPI application's routing tree and dependencies
5
5
  Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
6
6
  Project-URL: Source, https://github.com/allmonday/fastapi-voyager
@@ -9,7 +9,7 @@ fastapi_voyager/render_style.py,sha256=mPOuChEl71-3agCbPwkMt2sFmax2AEKDI6dK90eFP
9
9
  fastapi_voyager/server.py,sha256=MUc_ia3_QIbYQ8VenOxv3CEYVTiPDs7e_H40YoalxZs,8970
10
10
  fastapi_voyager/type.py,sha256=zluWvh5vpnjXJ9aAmyNJTSmXZPjAHCvgRT5oQRAjHrg,2104
11
11
  fastapi_voyager/type_helper.py,sha256=FmfrZAI3Z4uDdh3sH_kH7UGoY6yNVPapneSN86qY_wo,10209
12
- fastapi_voyager/version.py,sha256=MNw2WhSygJJXBlu1ZTOV_kDUGMOPYB98_73ZGP4k0S4,49
12
+ fastapi_voyager/version.py,sha256=mV_h7Kelwuchi4ZvsHaY1gjoGeeu7EK0TqrMYIGLwgI,49
13
13
  fastapi_voyager/voyager.py,sha256=4vonmL-xt54C5San-DRBq4mjoV8Q96eoWRy68MJ1IJw,14169
14
14
  fastapi_voyager/templates/dot/cluster.j2,sha256=I2z9KkfCzmAtqXe0gXBnxnOfBXUSpdlATs3uf-O8_B8,307
15
15
  fastapi_voyager/templates/dot/cluster_container.j2,sha256=2tH1mOJvPoVKE_aHVMR3t06TfH_dYa9OeH6DBqSHt_A,204
@@ -30,8 +30,8 @@ fastapi_voyager/web/graphviz.svg.js,sha256=deI815RgxpZ3_MpELeV-TBYy2MVuUvZtQOHfS
30
30
  fastapi_voyager/web/index.html,sha256=lA59Op4u1bb-B105Iadn2KN_n11-AtXFdJglUezZIDg,23225
31
31
  fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
32
32
  fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
33
- fastapi_voyager/web/store.js,sha256=h9WLKgFdmHrF9riirX-3FnCODZzeNZcvnxqZvfFnXLw,2020
34
- fastapi_voyager/web/vue-main.js,sha256=49iJ_uoXde-jcYJpS-_Gd10zGuxo_nhp1-LAAsFzyVc,20034
33
+ fastapi_voyager/web/store.js,sha256=zjmtx1HGN_umfyOQVwQgET-2V5hvXbS50YgXCr294Ok,14370
34
+ fastapi_voyager/web/vue-main.js,sha256=1s11NOjILgjncJ4WbN1DGzTeLUb0a2-LYIA3lUIX8Bc,10564
35
35
  fastapi_voyager/web/component/demo.js,sha256=sAklFGhKGmMy9-ofgOw2oPIidAoIOgHu6yvV51L_MAA,350
36
36
  fastapi_voyager/web/component/render-graph.js,sha256=9wnO70n3eyPKTpa744idgs5PSwgvzbfv4InZ68eEOKs,2454
37
37
  fastapi_voyager/web/component/route-code-display.js,sha256=a823nBz3EEjutW2pfi73rcF3hodCBmgYNmuZi94sXE4,3615
@@ -43,8 +43,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
43
43
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
44
44
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
45
45
  fastapi_voyager/web/icon/site.webmanifest,sha256=GRozZ5suTykYcPMap1QhjrAB8PLW0mbT_phhzw_utvQ,316
46
- fastapi_voyager-0.15.2.dist-info/METADATA,sha256=O7X7HJ1JMuXk_bJJ93gre1ESQOaeyBxWj4UtYtku1jo,8513
47
- fastapi_voyager-0.15.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
- fastapi_voyager-0.15.2.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
- fastapi_voyager-0.15.2.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
- fastapi_voyager-0.15.2.dist-info/RECORD,,
46
+ fastapi_voyager-0.15.3.dist-info/METADATA,sha256=_NFcPd07mWHADnTH01kf6c2y0Lw9AQgqQNnHMK0Y5xE,8513
47
+ fastapi_voyager-0.15.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ fastapi_voyager-0.15.3.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
+ fastapi_voyager-0.15.3.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
+ fastapi_voyager-0.15.3.dist-info/RECORD,,