The daily briefing needs AND: today's rules engine was OR-only. The
match semantics become a per-collection column
(collections.rule_match_mode, 'any' legacy default | 'all'), flipped on
its own endpoint (PUT /api/collections/:id/match-mode, owner-only —
nothing else on the row changes) and surfaced in the rules page as an
ANY/ALL toggle with matching copy.
Episode rule fields join the vocabulary (plan item 2.4): feed (the
multi-select rides the new `in` operator — comma-separated feed ids),
played (the canonical finished predicate — viewer progress ≥ 0.999,
same as hide_finished), downloaded, published_within (N days), plus
title/description contains. The rules candidate query now carries the
viewer's progress (LEFT JOIN reading_progress) so played/unplayed is
per-user, and TestRules/preview speak the SAME evaluator: the handler's
duplicated field switch is deleted and delegates to
CollectionService.FieldMatches — one vocabulary, one semantics.
Legitimate semantics note: in ANY mode `played:false` matches unplayed
BOOKS too (everything with no progress row) — correct per-rule
behavior, and exactly why the briefing mode exists.
Backward compatibility: every legacy row defaults to 'any' (the old
OR), UpdateCollection keeps the stored mode when the form doesn't send
one, and the evaluator treats absent data honestly (no date → no
match; no progress → unplayed).
Pinned by TestPodcastSmartCollections: the AND-mode briefing
(feed∈{A} ∧ unplayed ∧ <7d = exactly the new episode), OR-mode
widening, owner-only flips, the preview honoring the mode, and
downloaded/contains rules. Also fixes a latent bug the new query
surfaced: UpdateCollectionViewSettings referenced a nonexistent
updated_at column (never exercised until now — the new sibling query
was written without it).
40 lines
2.0 KiB
Go
40 lines
2.0 KiB
Go
package router
|
|
|
|
func registerCollectionsRoutes(cfg *Config) {
|
|
e := cfg.Echo
|
|
|
|
// JWT middleware for protected routes
|
|
jwtMiddleware := createJWTMiddleware(cfg)
|
|
protected := e.Group("/api", jwtMiddleware)
|
|
|
|
// Collections API routes
|
|
collections := protected.Group("/collections")
|
|
collections.GET("", cfg.CollectionHandler.GetCollections)
|
|
collections.POST("", cfg.CollectionHandler.CreateCollection)
|
|
collections.GET("/:id", cfg.CollectionHandler.GetCollection)
|
|
collections.PUT("/:id", cfg.CollectionHandler.UpdateCollection)
|
|
collections.DELETE("/:id", cfg.CollectionHandler.DeleteCollection)
|
|
collections.GET("/:id/books", cfg.CollectionHandler.GetBookCollections)
|
|
collections.POST("/:id/books", cfg.CollectionHandler.AddBooks)
|
|
collections.PUT("/:id/items/order", cfg.CollectionHandler.ReorderItems)
|
|
collections.PUT("/:id/match-mode", cfg.CollectionHandler.SetMatchMode)
|
|
collections.DELETE("/:id/books/:bookId", cfg.CollectionHandler.RemoveBook)
|
|
collections.POST("/:id/books/bulk-remove", cfg.CollectionHandler.BulkRemoveBooks)
|
|
collections.POST("/bulk-add-books", cfg.CollectionHandler.HandleBulkAddBooks)
|
|
collections.POST("/test-rules", cfg.CollectionHandler.TestRules)
|
|
collections.POST("/preview", cfg.CollectionHandler.PreviewCollection)
|
|
|
|
// Smart collection rule CRUD (the rules builder persists through
|
|
// these; type follows rules presence — first rule = smart).
|
|
collections.POST("/:id/rules", cfg.CollectionHandler.AddRule)
|
|
collections.PUT("/:id/rules/:ruleId", cfg.CollectionHandler.UpdateRule)
|
|
collections.DELETE("/:id/rules/:ruleId", cfg.CollectionHandler.RemoveRule)
|
|
|
|
// Device shelf mapping routes
|
|
deviceCollections := protected.Group("/devices/:id/collections")
|
|
deviceCollections.GET("", cfg.CollectionHandler.GetDeviceMappings)
|
|
deviceCollections.POST("", cfg.CollectionHandler.CreateDeviceMapping)
|
|
deviceCollections.PUT("/:collectionId", cfg.CollectionHandler.UpdateDeviceMapping)
|
|
deviceCollections.DELETE("/:collectionId", cfg.CollectionHandler.DeleteDeviceMapping)
|
|
}
|