docs(lookup): expand Section 4 with override model, group key invariants

Documents the root cause of the timezone collapse bug and how to avoid it
in future data imports. Covers:
- group as the dedup identity key (not a display label), per lookup type
- correct way to add/update/remove account and object overrides
- hard invariant for time_zone: group must equal name
- verification query to catch bad seed data before it ships
- frontend keying guidance: use group, not id or name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-23 17:56:50 -04:00
parent 15b5084df3
commit c7f1341b1e

View File

@@ -72,35 +72,117 @@ Modify data in the system.
## 4. V3 Uniform Lookup System
The V3 Lookup system provides a hierarchical, deduplicated interface for standardized tables (Countries, Timezones, etc.). It supports global defaults, account overrides, and site-specific whitelisting.
The V3 Lookup system provides a hierarchical, deduplicated interface for standardized reference tables (Countries, Timezones, etc.). It supports global defaults, account-level overrides, and object-level overrides, with optional site-specific whitelisting.
### How the hierarchy works
Each lookup table (`lu_v3_country`, `lu_v3_time_zone`, etc.) can hold multiple rows for the same logical item at different scopes:
| Scope | `account_id` | `for_type` / `for_id` | Wins over |
|---|---|---|---|
| Global default | `NULL` | `NULL` / `NULL` | nothing |
| Account override | set | `NULL` / `NULL` | Global default |
| Object override | set | set | Account override + Global default |
The API uses `ROW_NUMBER() PARTITION BY group` to collapse all rows for the same item down to the single highest-priority winner before returning results. **`group` is the identity key** — it is what makes two rows "the same item competing for priority."
> [!IMPORTANT]
> **The `group` field is not a display label.** It is the deduplication key. Each lookup type uses a different natural key for `group`:
>
> | Lookup type | `group` value | Example |
> |---|---|---|
> | `country` | ISO alpha-2 code | `"US"`, `"CA"`, `"GB"` |
> | `country_subdivision` | subdivision code | `"US-NY"`, `"CA-ON"` |
> | `time_zone` | IANA timezone name | `"America/New_York"`, `"US/Eastern"` |
>
> For `time_zone`, `group` and `name` must always be identical — there is no concept of "override all US timezones as a group." Each timezone is its own identity.
### A. List Lookups
Retrieve a ranked and filtered list of lookup items.
Retrieve the deduplicated, ranked list for a lookup type.
* **Endpoint:** `GET /v3/lookup/{lu_type}/list`
* **Available Types:** `country`, `country_subdivision`, `time_zone`
* **Parameters:**
* `site_id` (Optional): Random ID of the site to apply a **Whitelist Policy**.
* `only_priority` (Optional): Set to `true` to return only high-priority items (e.g., common time zones).
* `for_type` / `for_id` (Optional): Context for object-specific overrides.
* `include_disabled` (Optional): Set to `true` to see shadowed/disabled records.
* `site_id` (Optional): Random ID of the site applies a **Whitelist Policy** (see §C).
* `only_priority` (Optional): `true` returns only `priority=1` items (e.g., common time zones).
* `for_type` / `for_id` (Optional): Object context — activates object-level override matching.
* `include_disabled` (Optional): `true` includes shadowed/disabled records (useful for admin views).
**Frontend keying:** Always key Svelte `{#each}` blocks on `group`, not `id` or `name`. `group` is guaranteed unique in the response. Keying on `id` will break if an account override wins (different `id`, same logical item).
### B. Resolve Identity
Resolves a string (code, group, or name) to a single record.
Resolves a string to a single lookup record.
* **Endpoint:** `GET /v3/lookup/{lu_type}/resolve?q=VALUE`
* **Usage:** Use this when you have an external code (e.g., ISO "US") and need the full Aether record.
* **Usage:** Use when you have an external code (e.g., ISO `"US"`) and need the full Aether record. Scans `name`, `group`, and other identity fields.
### C. Site Whitelist Policy
To limit lookups for a specific site, add a `lookup_policy` to the `site.cfg_json` field.
**Schema:**
To restrict which lookup items appear for a specific site, add a `lookup_policy` to `site.cfg_json`:
```json
{
"lookup_policy": {
"country": ["US", "CA", "GB"],
"time_zone": ["America/New_York"]
"time_zone": ["America/New_York", "US/Eastern"]
}
}
```
*Note: Whitelist values must match the `group` field in the database.*
> **Whitelist values must match the `group` field** — i.e., the natural key for that type (ISO code for country, IANA name for time zone). Using a display name will silently return no results for that item.
### D. Adding and managing client overrides
When a client needs a customized label or wants to hide/reorder lookup items, create override records rather than modifying global defaults.
**Rules:**
1. **Never modify global default rows** (`account_id = NULL`). Those are shared across all accounts. Any change there affects every client.
2. **Set `group` to the exact same value as the global default row** for the item you are overriding. If `group` doesn't match, the override creates a new item instead of replacing the existing one.
3. **Set `account_id`** to the client's account ID. Leave `for_type` / `for_id` null unless the override is specific to a single object (e.g., one site).
**Example — rename "US/Eastern" for one account:**
```sql
INSERT INTO lu_v3_time_zone
(account_id, name, name_override, `group`, enable, priority, sort)
VALUES
(42, 'US/Eastern', 'Eastern Time (Client Label)', 'US/Eastern', 1, 1, 50);
```
The `name_override` field is the display label the frontend should prefer when set. `group = 'US/Eastern'` ensures this row competes with — and wins over — the global default in the `PARTITION BY group` deduplication.
**To disable an item for one account** (hide it from their dropdowns):
```sql
INSERT INTO lu_v3_time_zone
(account_id, name, `group`, enable)
VALUES
(42, 'US/Samoa', 'US/Samoa', 0);
```
Setting `enable = 0` on an account-scoped row shadows the global default for that account only.
**To remove a client override** (revert to global default):
Simply delete the row where `account_id = <client>` and `group = '<item>'`. The global default row is unaffected and immediately resumes winning.
### E. Adding new global lookup items
When seeding new lookup data (e.g., adding timezones in bulk):
1. Set `group = name` for every row (for `time_zone`). This is a hard invariant — if `group` is set to a regional label like `"United States"` instead of the timezone name, the entire group collapses to a single winner and all but one entry disappear from the API response.
2. Set `account_id = NULL` and `for_type = NULL` / `for_id = NULL` for global defaults.
3. After seeding, verify with:
```sql
-- Should return 0 rows; any result means multiple items will collapse into one
SELECT `group`, COUNT(*) AS cnt
FROM lu_v3_time_zone
WHERE account_id IS NULL
GROUP BY `group`
HAVING cnt > 1;
```
---