refactor(collections): retire the legacy rules surface — tree-only storage
The pre-tree rules machinery is deleted: no row ever carried a rules array or a non-default match mode (verified in the dev DB), the web never read the envelope keys, and the Android app consumes none of it. Dead code gets no documentation. Removed: - PUT /api/collections/:id/match-mode — endpoint, route, and the UpdateCollectionMatchMode query. The tree's root mode owns any|all. - auto_assign_rules / rule_match_mode from every envelope (create/list/detail/update), from the create/update request shapes, and from the schema: both columns DROPPED (schema.sql carries idempotent DROP COLUMN IF EXISTS heals for existing DBs; sqlc regenerated). - The rules CRUD service layer (AddRule/SetRuleEnabled/RemoveRule + validateRule + UpdateCollectionRulesJSON), ParseRules / EnabledRules / IsSmartCollection, RulesToFilterTree (+ the SSR preload branch), the legacy branch of EvaluateSmartMembership, and SmartEvalOptions.MatchMode. IsCollectionSmart = tree presence; create-path isSmart = collection_type=smart OR a filter_tree. Kept (live consumers): POST /api/collections/preview still evaluates rules payloads in memory (the custom-section builder's preview), and include_auto stays on the list (the Android app sends it). The custom-section builder's SAVE was already broken — its stringified auto_assign_rules payload failed request binding (probed live: 400 invalid request). Create now rejects that payload loudly with a 400 naming the retirement, instead of silently creating a hollow static section; custom-section-builder.md carries the flag. The builder's tree migration remains the flagged follow-up. Test-side (asymmetry rule — deleted behavior, deleted tests): TestPodcastSmartCollections rewritten onto trees (mode lives in the tree root), match-mode subtests and tree_wins_over_legacy_rules deleted, TestRulesToFilterTree deleted, contract-pin envelope keys retracted per the contract_pins 2026-10-31 retirement entry. Mirrors hand-synced: openapi.yaml, bruno create/update, docs/api/*.md, docs/schema.go, api.d.ts. Full container suite green (155s).
This commit is contained in:
1 parent
ab98015190
commit
a86bdac87e
27 files changed
+239
-1093
No files matched your search
@@ -545,3 +545,35 @@ Test-side: TestCollectionsRLS pins the cross-user matrix end-to-end;
|
||||
fixture writes to non-system collections run as the row's OWNER
|
||||
(scopeExecAs/ownerSeedCtx) because the admin write-escape covers
|
||||
system collections only.
|
||||
|
||||
## 2026-10-31 — legacy collection rules surface RETIRED (tree-only storage)
|
||||
|
||||
The pre-tree rules machinery is gone — no production row ever carried a
|
||||
rules array or a non-default match mode (verified in the dev DB), the
|
||||
web never reads the envelope keys, and the Android app consumes none of
|
||||
it. Deletions:
|
||||
|
||||
- `PUT /api/collections/:id/match-mode` — endpoint retired (it had no
|
||||
doc page). The tree's root `mode` owns any|all semantics now.
|
||||
- Envelope: `auto_assign_rules` and `rule_match_mode` are REMOVED from
|
||||
the create/list/detail/update payloads. This entry retracts the
|
||||
2026-10-30 pins that listed them. `collection_type` and `is_smart`
|
||||
stay.
|
||||
- Request: `auto_assign_rules`/`rule_match_mode` are no longer accepted
|
||||
on create/update. A create carrying `auto_assign_rules` answers 400
|
||||
naming the retirement — the custom-section builder's legacy save
|
||||
(already failing to bind, verified live) fails loudly instead of
|
||||
silently creating a hollow static section.
|
||||
- Storage: the `collections.auto_assign_rules` and
|
||||
`collections.rule_match_mode` columns are DROPPED (idempotent
|
||||
`DROP COLUMN IF EXISTS` in schema.sql).
|
||||
- Service: the rules CRUD layer (AddRule/SetRuleEnabled/RemoveRule +
|
||||
validateRule + the UpdateCollectionRulesJSON query),
|
||||
ParseRules/EnabledRules/IsSmartCollection, RulesToFilterTree (+ the
|
||||
SSR preload branch), and the legacy branch of EvaluateSmartMembership
|
||||
are deleted. IsCollectionSmart = tree presence.
|
||||
|
||||
KEPT (live consumers): `POST /api/collections/preview` still accepts
|
||||
`rules` and evaluates them in memory (the custom-section builder's
|
||||
preview button); `include_auto` on the list stays (the Android app
|
||||
sends it). The builder's SAVE remains the flagged §12 tree migration.
|
||||
@@ -23,7 +23,6 @@ Creating a collection whose name already exists for the user returns **409**.
|
||||
"color": "#00FF00",
|
||||
"icon": "📖",
|
||||
"collection_type": "static",
|
||||
"auto_assign_rules": [],
|
||||
"view_settings": { "kobo": { "view_mode": "grid" } }
|
||||
}
|
||||
```
|
||||
@@ -39,7 +38,6 @@ Creating a collection whose name already exists for the user returns **409**.
|
||||
"color": "#00FF00",
|
||||
"icon": "📖",
|
||||
"collection_type": "static",
|
||||
"auto_assign_rules": null,
|
||||
"view_settings": { "kobo": { "view_mode": "grid" } },
|
||||
"book_count": 0,
|
||||
"created_at": "2026-10-24 13:57:01.81234 +0000 UTC"
|
||||
@@ -57,20 +55,9 @@ RFC3339. Treat it as display text; do not parse it as an ISO timestamp.
|
||||
| description | string | no | |
|
||||
| color | string | no | hex color, e.g. `"#FF5733"` |
|
||||
| icon | string | no | emoji |
|
||||
| collection_type | string | no | `"static"` (default) or `"smart"`; also implied by non-empty `auto_assign_rules` |
|
||||
| auto_assign_rules | array | no | rule objects, see below |
|
||||
| collection_type | string | no | `"static"` (default) or `"smart"`; also implied by a `filter_tree` |
|
||||
| view_settings | object | no | per-device display preferences |
|
||||
|
||||
### Auto-assign rule object
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
| ----- | ---- | -------- | ----- |
|
||||
| id | string | no | rule id |
|
||||
| field | string | yes | one of `genre`, `series`, `author`, `language`, `publisher`, `copyright_year`, `tags` |
|
||||
| operator | string | yes | one of `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `greater_than`, `less_than` |
|
||||
| value | string | yes | value to compare against |
|
||||
| priority | number | no | 1-10; higher values take precedence |
|
||||
| enabled | boolean | no | absent = enabled (rules written before this field existed keep matching) |
|
||||
| filter_tree | object | no | the unified filter tree (qf DSL); presence makes the collection smart |
|
||||
|
||||
## Errors
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ GET /api/collections/550e8400-e29b-41d4-a716-446655440000
|
||||
"description": "Books I want to read soon",
|
||||
"color": "#00FF00",
|
||||
"icon": "📖",
|
||||
"auto_assign_rules": null,
|
||||
"view_settings": { "kobo": { "view_mode": "grid" } },
|
||||
"books": [],
|
||||
"book_count": 0,
|
||||
@@ -57,8 +56,8 @@ GET /api/collections/550e8400-e29b-41d4-a716-446655440000
|
||||
| description | string | |
|
||||
| color | string | |
|
||||
| icon | string | |
|
||||
| auto_assign_rules | array \| null | |
|
||||
| view_settings | object | |
|
||||
| filter_tree | object \| null | the unified filter tree (qf DSL); null = static |
|
||||
| books | array | book cards in this collection |
|
||||
| book_count | number | length of `books` |
|
||||
| is_smart | boolean | |
|
||||
|
||||
@@ -29,7 +29,6 @@ GET /api/collections?include_auto=true&sort_by=name
|
||||
"description": "Books I want to read soon",
|
||||
"color": "#00FF00",
|
||||
"icon": "📖",
|
||||
"auto_assign_rules": null,
|
||||
"created_at": "2026-10-24 13:57:01.81234 +0000 UTC",
|
||||
"book_count": 12,
|
||||
"is_smart": false
|
||||
@@ -65,7 +64,6 @@ GET /api/collections?include_auto=true&sort_by=name
|
||||
| description | string | |
|
||||
| color | string | |
|
||||
| icon | string | |
|
||||
| auto_assign_rules | array \| null | raw rules JSON; non-null implies smart |
|
||||
| created_at | string | Go `time.String()` format |
|
||||
| book_count | number | live count; respects `library_id` for smart collections |
|
||||
| is_smart | boolean | |
|
||||
|
||||
@@ -24,7 +24,6 @@ set `view_settings.hide_finished` / `view_settings.show_up_next` directly.
|
||||
"description": "Updated description",
|
||||
"color": "#33CCFF",
|
||||
"icon": "📚",
|
||||
"auto_assign_rules": [],
|
||||
"view_settings": { "hide_finished": true },
|
||||
"hide_finished": ["true"],
|
||||
"show_up_next": ["true"]
|
||||
@@ -41,7 +40,6 @@ set `view_settings.hide_finished` / `view_settings.show_up_next` directly.
|
||||
"description": "Updated description",
|
||||
"color": "#33CCFF",
|
||||
"icon": "📚",
|
||||
"auto_assign_rules": null,
|
||||
"view_settings": { "hide_finished": true, "show_up_next": true },
|
||||
"created_at": "2026-10-24 13:57:01.81234 +0000 UTC"
|
||||
}
|
||||
@@ -57,8 +55,8 @@ set `view_settings.hide_finished` / `view_settings.show_up_next` directly.
|
||||
| description | string | no | |
|
||||
| color | string | no | |
|
||||
| icon | string | no | |
|
||||
| auto_assign_rules | array | no | replaces the whole rule set (rule object shape: see Create Collection) |
|
||||
| view_settings | object | no | merged into stored settings |
|
||||
| filter_tree | object | no | replaces the stored filter tree (omitting it keeps the stored one) |
|
||||
| hide_finished | array | no | single-element string array `["true"]`/`["false"]`; merged into view_settings |
|
||||
| show_up_next | array | no | same shape as `hide_finished` |
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ Creates a new custom collection with filter rules and/or manual book selection.
|
||||
|
||||
**Request Body:**
|
||||
|
||||
**Status (2026-10-31):** the rules-array storage this payload targets
|
||||
was retired — collections carry filter trees now. The server rejects
|
||||
this save with a 400 naming the retired fields; the builder needs its
|
||||
flagged tree migration (§12) before saving works again.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Custom Section",
|
||||
|
||||
@@ -229,7 +229,6 @@ paths:
|
||||
"color": "#00FF00",
|
||||
"icon": "📖",
|
||||
"collection_type": "static",
|
||||
"auto_assign_rules": [],
|
||||
"view_settings": { "kobo": { "view_mode": "grid" } }
|
||||
}
|
||||
responses:
|
||||
@@ -308,7 +307,6 @@ paths:
|
||||
"description": "Updated description",
|
||||
"color": "#33CCFF",
|
||||
"icon": "📚",
|
||||
"auto_assign_rules": [],
|
||||
"view_settings": { "hide_finished": true },
|
||||
"hide_finished": ["true"],
|
||||
"show_up_next": ["true"]
|
||||
|
||||
Reference in new issue
Block a user