docs: Obsidian-style vault under docs/
Add a navigable Obsidian vault as the project's first-class technical documentation. Notes cross-reference with [[wikilinks]]; docs/index.md is the Map of Content. New notes: index.md MOC, entry point architecture.md what the extension owns vs what lives outside data-model.md entity-by-entity schema reference menu-tree.md the arbitrary-depth tree concept order-flow.md state machine + invoice listener + print nostr-layer.md kinds 0/30402/5/1059, signing, t-tags api-reference.md endpoint catalog by audience cms.md Vue 3 + Quasar 2 UMD conventions, q-tree webapp-integration.md multi-restaurant cart pattern + atomicity glossary.md domain terms Existing notes (kept as-is): adr-0001-menu-tree.md the storage choice rationale design-conversation.md trimmed transcript README.md adds a Documentation section pointing at docs/index.md with the headline note list. Each note links to ~3-5 others; the vault forms a connected graph. A project-level memory rule (saved outside the repo) commits us to keeping these docs in sync as the code evolves: any commit that materially changes schema, API, order flow, Nostr surface, CMS conventions, or webapp integration must update the relevant note(s) in the same commit.
This commit is contained in:
parent
7f7915a041
commit
42a8b08a5b
11 changed files with 1015 additions and 0 deletions
91
docs/menu-tree.md
Normal file
91
docs/menu-tree.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Menu tree
|
||||
|
||||
Real menus are nested:
|
||||
*Drinks → Hot Beverages → Coffee-based → Espressos*. The extension
|
||||
models this as a single self-referential `menu_nodes` table.
|
||||
|
||||
## Storage shape
|
||||
|
||||
Adjacency list (`parent_id` self-FK) plus two denormalized columns:
|
||||
|
||||
- `path` — `'rootid'` or `'rootid/childid/...'` (slash-separated ids).
|
||||
- `depth` — integer 0..3 (zero-indexed; cap is 4 levels).
|
||||
|
||||
See [[data-model]] for the column list and [[adr-0001-menu-tree]] for
|
||||
why this shape was chosen over a closure table or `ltree`.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Max depth is 4** (`MAX_MENU_DEPTH = 3`). Creates that would
|
||||
exceed the cap return HTTP 400. Moves that would push descendants
|
||||
past the cap also return 400.
|
||||
- **Items can attach to any node** — the leaf-only constraint of
|
||||
the legacy two-level shape is gone. A "Drinks" node can hold its
|
||||
own drinks AND nest sub-categories below it.
|
||||
- **Cycle prevention** on move: the new parent's path must not
|
||||
contain the moved node's id.
|
||||
- **Cascade-delete detaches items** rather than wiping them. Items
|
||||
carry `nostr_event_id` and revenue history, so the operator
|
||||
re-homes orphans through the [[cms]] rather than losing them.
|
||||
|
||||
## Operations
|
||||
|
||||
| Op | Cost | How |
|
||||
|---|---|---|
|
||||
| Children of node | O(log n) | `WHERE parent_id = :id` (indexed) |
|
||||
| Subtree of node | O(log n) | `WHERE path LIKE :p \|\| '%'` (indexed) |
|
||||
| Ancestors of node | O(depth) | split `path`, fetch by id |
|
||||
| Cycle check on move | O(depth) | id in new parent's `path.split('/')` |
|
||||
| Max-depth check | O(1) | integer compare |
|
||||
| Move subtree | one statement | `path = new_prefix \|\| SUBSTR(path, len(old)+1)` |
|
||||
| Build full tree for restaurant | O(n+m) Python | one `SELECT *` → assemble in memory |
|
||||
|
||||
## Move
|
||||
|
||||
The load-bearing operation. Single-statement subtree rewrite:
|
||||
|
||||
```sql
|
||||
UPDATE restaurant.menu_nodes
|
||||
SET path = :new_prefix || SUBSTR(path, :old_len + 1),
|
||||
depth = depth + :delta
|
||||
WHERE path = :old_path
|
||||
OR path LIKE :old_path || '/%'
|
||||
```
|
||||
|
||||
`SUBSTR` is 1-indexed on both SQLite and Postgres, so `len(old_path)
|
||||
+ 1` slices the old prefix off correctly. Followed by a separate
|
||||
`UPDATE menu_nodes SET parent_id = :new_pid WHERE id = :node_id` for
|
||||
the moved root (descendants keep their parent_id; only paths +
|
||||
depths shift).
|
||||
|
||||
Implementation lives in `crud.move_menu_node`.
|
||||
|
||||
## Tree assembly
|
||||
|
||||
Customers and the [[cms]] both want the whole tree in one call.
|
||||
`crud.get_menu_tree(restaurant_id)`:
|
||||
|
||||
1. `SELECT * FROM menu_nodes WHERE restaurant_id = :rid ORDER BY depth, sort_order, time`
|
||||
2. `SELECT * FROM menu_items WHERE restaurant_id = :rid`
|
||||
3. Build `by_id: dict[id, MenuNode]` in Python.
|
||||
4. Walk rows, attaching each non-root to `by_id[parent_id].children`.
|
||||
5. Walk items, attaching each to `by_id[node_id].items`.
|
||||
|
||||
For typical restaurant sizes (5–50 nodes, 10–200 items) this is
|
||||
microseconds. Identical on SQLite + Postgres, no recursive CTE
|
||||
needed.
|
||||
|
||||
## Items at non-leaf levels
|
||||
|
||||
A node can have BOTH children AND items attached. The [[cms]]
|
||||
renders both at each level. The [[nostr-layer]] tags items with
|
||||
their full ancestor chain (root-first, slugified) so a customer
|
||||
filtering for `#t=hot-beverages` finds everything under that branch
|
||||
regardless of how deeply it nests.
|
||||
|
||||
## See also
|
||||
|
||||
- [[data-model]] — columns + indexes
|
||||
- [[cms]] — the q-tree builder UI
|
||||
- [[nostr-layer]] — ancestor `t` tags
|
||||
- [[adr-0001-menu-tree]] — adjacency vs. closure trade-off
|
||||
Loading…
Add table
Add a link
Reference in a new issue