You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* fix: display area badges scroll bug
* feat: bump redis to rc.12
* feat: update readme file to include details about project internals
* refactor: schema parser to support more errors
* fix: update query types and remove $ne
* fix: many minor fixes for redis search
* fix: loading state of query wizard
* fix: minor style fixes for search-empty-state
@@ -50,3 +48,72 @@ export default function Page() {
50
48
)
51
49
}
52
50
```
51
+
52
+
---
53
+
54
+
## Codebase Internals
55
+
56
+
### Query Data Flow (UI ↔ Code ↔ Store ↔ Redis)
57
+
58
+
The query is stored in the zustand store as a **JS object literal string** (not JSON — keys are unquoted like `$and:` instead of `"$and":`). This single string is the shared source of truth between both the UI query builder and the code editor.
59
+
60
+
**Store shape:**`ValuesSearchFilter.queries` is a `Record<string, string>` mapping `indexName → queryString`. Each index remembers its own query independently.
61
+
62
+
**Code editor flow (`query-builder.tsx`):** Prepends `"const query: Query = "` to the stored string for Monaco. On edit, strips that prefix and writes the raw string to the store. Monaco gets type definitions generated from the index schema, giving autocomplete.
63
+
64
+
**UI query builder flow:** The UI works with a tree (`QueryState` — groups and conditions with IDs). `useQueryStateSync` handles bidirectional sync:
65
+
66
+
-**Store → tree:**`parseQueryString` (`query-parser.ts`) parses the string into `QueryNode`s. Runs on mount and when the store query changes externally (e.g. user edited in code mode then switched to UI).
67
+
-**Tree → store:** When the UI mutates the tree, `stringifyQueryState` (`query-stringify.ts`) converts back to a string and writes to the store.
68
+
-**Sync guard:**`isOurUpdate` ref prevents the hook from re-parsing its own writes. Only external changes trigger re-parsing.
69
+
70
+
**Limitation:**`$must`/`$should` combination is not supported in the UI builder — `hasMustShouldCombination` check blocks switching to UI mode when this pattern exists. The UI normalizes `$must`→`$and` and `$should`→`$or`.
71
+
72
+
**Store → Redis:**`KeysProvider` (`use-keys.tsx`) reads `valuesSearch.query` from the store, parses it with `parseJSObjectLiteral` into a plain object, and passes it as the `filter` to `redis.search.index().query()`.
73
+
74
+
### Schema Data Flow
75
+
76
+
Schemas define the structure of a search index. Two formats exist:
`parseSchemaFromEditorValue` (`schema-parser.ts`) converts editor → API by recursively walking the builder calls, flattening nested `s.object()` calls into dot-separated keys, and mapping `s.string()` → `"TEXT"`, `s.number()` → `"F64"`, etc.
82
+
83
+
`schemaToEditorValue` converts API → editor by unflattening dot-notation keys into a nested object, then rendering back to the builder DSL.
84
+
85
+
`generateSchemaTypeDefinitions` produces TypeScript class declarations (the `s` builder, field builders, `Schema` type) that are injected into Monaco for autocomplete in the schema editor. The type definitions in `search-types-file.ts` mirror the `@upstash/redis` SDK types.
86
+
87
+
### UI Query Builder Mutations
88
+
89
+
All mutations go through `QueryBuilderUIProvider` (`query-builder-context.tsx`), which exposes four operations via context: `updateNode`, `deleteNode`, `addChildToGroup`, `moveNode`.
90
+
91
+
Every node in the tree has a unique `id`. All operations traverse the tree by ID, apply the change immutably, and return a new tree. The flow: component calls e.g. `updateNode(id, updates)` → `setQueryState` from `useQueryStateSync` receives a modifier → modifier gets a `structuredClone` of current state → tree is modified → result is stringified and written to the zustand store.
-**`DraggableItem`** — wraps each condition row and group row, provides the drag handle
100
+
-**`DropIndicator`** (`drop-zone.tsx`) — a thin horizontal line rendered _between_ each child in a group. Each one is a droppable zone.
101
+
-**`EmptyGroupDropZone`** (`drop-zone.tsx`) — rendered inside empty groups as a dashed "Add a condition" button that also accepts drops
102
+
-**`QueryDragOverlay`** — portal overlay showing a preview of the dragged node while dragging
103
+
-**`QueryDndProvider`** (`dnd-context.tsx`) — wraps the tree, owns the `@dnd-kit``DndContext`, handles all drag events
104
+
105
+
**How drop zones work:**`QueryGroup` renders a `DropIndicator` before each child and one at the end. Each drop zone has an ID following the pattern `drop-{groupId}-{childId}` (insert before that child) or `drop-{groupId}-end` (append). This encoding lets `onDragEnd` know both _which group_ and _which position_ to insert at by just parsing the drop zone ID.
106
+
107
+
On drop, `dnd-context.tsx` resolves the source/target from the IDs, handles edge cases (no-op for same position, prevents dropping a group into its own descendants), and calls `moveNode` from the query builder context.
108
+
109
+
### Keys List (`useKeys`)
110
+
111
+
`KeysProvider` wraps `DatabrowserInstance` and provides fetched keys via `useKeys()`. It's at root level because multiple children need it: the **sidebar** renders the key list, and the **data display** uses `useKeyType(selectedKey)` (which looks up the type from the fetched keys array) to decide which editor to render for the selected key, and uses `query.isLoading` to show a loading state.
112
+
113
+
**Dual-mode fetching** based on `isValuesSearchSelected`:
114
+
115
+
1.**Standard Redis SCAN:** Sends `SCAN` commands with optional `MATCH` pattern and `TYPE` filter. When SCAN returns empty pages (sparse databases + type filters), retries with increasing `COUNT` (`100 → 300 → 500`) to reduce round trips.
116
+
117
+
2.**Redis Search:** Calls `redis.search.index().query()` with the parsed filter object from the store, offset-based pagination, and `select: {}` (returns only keys + scores).
118
+
119
+
Uses React Query's `useInfiniteQuery` for cursor-based pagination. Key types from the scan are cached individually so `useFetchKeyType` (used elsewhere) doesn't need extra `TYPE` commands. Results are deduplicated since Redis SCAN can return duplicates across pages.
0 commit comments