11"""
2- url for local testing:
2+ Handler for catalog endpoint
3+
4+ URL for local testing:
35http://127.0.0.1:5001/schemessg-v3-dev/asia-southeast1/catalog
4- http://127.0.0.1:5001/schemessg-v3-dev/asia-southeast1/catalog?category=<category>
56http://127.0.0.1:5001/schemessg-v3-dev/asia-southeast1/catalog?agency=<agency>
67"""
78
89import json
9- from dataclasses import dataclass
10+ from dataclasses import asdict , dataclass
11+ from enum import Enum
1012from typing import Literal , Union
1113
1214from fb_manager .firebaseManager import FirebaseManager
1315from firebase_functions import https_fn , options
14- from firebase_functions .firestore_fn import DocumentSnapshot
1516from google .cloud .firestore_v1 import FieldFilter
16- from google .cloud .firestore_v1 .query_results import QueryResultsList
1717from loguru import logger
1818from utils .auth import verify_auth_token
19+ from utils .catalog_pagination import PaginationResult , get_paginated_results
1920from utils .cors_config import get_cors_headers , handle_cors_preflight
2021from utils .json_utils import safe_json_dumps
2122from werkzeug .datastructures import MultiDict
2223
2324
24- @dataclass
25- class CatalogRequestTarget :
26- ALL : Literal ["all" ] = "all"
27- CATEGORY : Literal ["category" ] = "category"
28- AGENCY : Literal ["agency" ] = "agency"
25+ ALLOWED_QUERY_PARAMS = {"agency" , "limit" , "cursor" , "is_warmup" }
2926
3027
31- @dataclass
32- class AllCatalogRequestParams :
33- target : CatalogRequestTarget .ALL
28+ class CatalogTarget (Enum ):
29+ ALL = "all"
30+ AGENCY = "agency"
31+
32+
33+ @dataclass (kw_only = True )
34+ class BaseCatalogRequestParams :
35+ target : CatalogTarget
36+ limit : int = 10
37+ cursor : str | None = None
3438
3539
3640@dataclass
37- class CategoryCatalogRequestParams :
38- target : CatalogRequestTarget .CATEGORY
39- category_name : str
41+ class AllCatalogRequestParams (BaseCatalogRequestParams ):
42+ target : Literal [CatalogTarget .ALL ]
4043
4144
4245@dataclass
43- class AgencyCatalogRequestParams :
44- target : CatalogRequestTarget .AGENCY
46+ class AgencyCatalogRequestParams ( BaseCatalogRequestParams ) :
47+ target : Literal [ CatalogTarget .AGENCY ]
4548 agency_name : str
4649
4750
48- CatalogRequestParams = Union [AllCatalogRequestParams , CategoryCatalogRequestParams , AgencyCatalogRequestParams ]
51+ CatalogRequestParams = Union [AllCatalogRequestParams , AgencyCatalogRequestParams ]
4952
5053
5154def create_firebase_manager () -> FirebaseManager :
@@ -106,27 +109,22 @@ def catalog(req: https_fn.Request) -> https_fn.Response:
106109 try :
107110 query_params = _parse_query_params (req .args )
108111 except ValueError as e :
109- logger .error ("Error parsing query parameters" , e )
112+ logger .exception ("Error parsing query parameters" , e )
110113 return https_fn .Response (
111114 response = json .dumps (
112- {
113- "error" : "Error parsing query parameters; only /catalog, /catalog?category=<category> and /catalog?agency=<agency> are supported"
114- }
115+ {"error" : "Error parsing query parameters; only /catalog and /catalog?agency=<agency> are supported" }
115116 ),
116117 status = 400 ,
117118 mimetype = "application/json" ,
118119 headers = headers ,
119120 )
120121
121122 try :
122- docs : QueryResultsList [DocumentSnapshot ] | None = None
123123 match query_params .target :
124- case CatalogRequestTarget .ALL :
125- docs = _handle_all_catalog_request (firebase_manager )
126- case CatalogRequestTarget .CATEGORY :
127- docs = _handle_category_catalog_request (firebase_manager , query_params )
128- case CatalogRequestTarget .AGENCY :
129- docs = _handle_agency_catalog_request (firebase_manager , query_params )
124+ case CatalogTarget .ALL :
125+ results = _handle_all_catalog_request (firebase_manager , query_params )
126+ case CatalogTarget .AGENCY :
127+ results = _handle_agency_catalog_request (firebase_manager , query_params )
130128 except Exception as e :
131129 logger .exception ("Unable to fetch scheme from firestore" , e )
132130 return https_fn .Response (
@@ -136,81 +134,105 @@ def catalog(req: https_fn.Request) -> https_fn.Response:
136134 headers = headers ,
137135 )
138136
139- if not docs :
137+ if results . data is None or len ( results . data ) == 0 :
140138 return https_fn .Response (
141139 response = json .dumps ({"error" : "No scheme found" }),
142140 status = 404 ,
143141 mimetype = "application/json" ,
144142 headers = headers ,
145143 )
146144
147- results = {"data" : [doc .to_dict () for doc in docs ]}
148145 return https_fn .Response (
149- response = safe_json_dumps (results ),
146+ response = safe_json_dumps (asdict ( results ) ),
150147 status = 200 ,
151148 mimetype = "application/json" ,
152149 headers = headers ,
153150 )
154151
155152
156- # TODO: Add support for pagination parsing
157153def _parse_query_params (query_params : MultiDict [str , str ]) -> CatalogRequestParams :
158154 """
159155 Parse request query parameters into CatalogRequestParams.
160156
161157 Supported:
162- - /catalog -> AllCatalogRequestParams
163- - /catalog?category=<name> -> CategoryCatalogRequestParams
164- - /catalog?agency=<name> -> AgencyCatalogRequestParams
158+ - /catalog -> AllCatalogRequestParams
159+ - /catalog?agency=<name> -> AgencyCatalogRequestParams
160+
161+ Raises:
162+ ValueError: If unsupported query parameters are provided.
165163 """
166164
167- category = query_params .get ("category" )
168- agency = query_params .get ("agency" )
165+ # Validate unknown query parameters
166+ unknown_params = set (query_params .keys ()) - ALLOWED_QUERY_PARAMS
167+ if unknown_params :
168+ raise ValueError (f"Unsupported query parameter(s): { ', ' .join (sorted (unknown_params ))} " )
169169
170- # Disallow ambiguous requests
171- if category and agency :
172- raise ValueError ("Invalid query; provide only one of 'category' or 'agency'" )
170+ # Retrieve relevant sub-catalog query parameters
171+ agency = query_params .get ("agency" )
173172
174- # /catalog?category=<>
175- if category :
176- if not category .strip ():
177- raise ValueError ("Invalid query; 'category' must be a non-empty value" )
178- return CategoryCatalogRequestParams (target = CatalogRequestTarget .CATEGORY , category_name = category )
173+ # Retrieve limit and cursor from query parameters
174+ limit = int (query_params .get ("limit" , "10" ))
175+ cursor = query_params .get ("cursor" , "" )
179176
180177 # /catalog?agency=<>
181178 if agency :
182179 if not agency .strip ():
183180 raise ValueError ("Invalid query; 'agency' must be a non-empty value" )
184- return AgencyCatalogRequestParams (target = CatalogRequestTarget .AGENCY , agency_name = agency )
181+ return AgencyCatalogRequestParams (target = CatalogTarget .AGENCY , agency_name = agency , limit = limit , cursor = cursor )
185182
186183 # /catalog
187- return AllCatalogRequestParams (target = CatalogRequestTarget .ALL )
188-
184+ return AllCatalogRequestParams (target = CatalogTarget .ALL , limit = limit , cursor = cursor )
189185
190- def _handle_all_catalog_request (firebase_manager : FirebaseManager ) -> QueryResultsList [DocumentSnapshot ] | None :
191- """Handle all catalog request."""
192186
193- ref = firebase_manager .firestore_client .collection ("schemes" )
194- return ref .get ()
187+ def _handle_all_catalog_request (
188+ firebase_manager : FirebaseManager ,
189+ query_params : AllCatalogRequestParams ,
190+ ) -> PaginationResult :
191+ """Retrieve all catalog entries using cursor pagination.
195192
193+ Args:
194+ firebase_manager: Firebase manager providing Firestore access.
196195
197- def _handle_category_catalog_request (
198- firebase_manager : FirebaseManager , query_params : CategoryCatalogRequestParams
199- ) -> QueryResultsList [DocumentSnapshot ] | None :
200- """Handle category catalog request."""
196+ Returns:
197+ PaginationResult:
198+ data: Documents for the current page.
199+ next_cursor: Cursor for the next page, or None if exhausted.
200+ has_more: Whether more results exist.
201+ """
202+ col = firebase_manager .firestore_client .collection ("schemes" )
201203
202- ref = firebase_manager .firestore_client .collection ("schemes" ).where (
203- filter = FieldFilter ("scheme_type" , "==" , query_params .category_name )
204+ return get_paginated_results (
205+ collection_ref = col ,
206+ cursor = query_params .cursor ,
207+ limit = query_params .limit ,
204208 )
205- return ref .get ()
206209
207210
208211def _handle_agency_catalog_request (
209212 firebase_manager : FirebaseManager , query_params : AgencyCatalogRequestParams
210- ) -> QueryResultsList [DocumentSnapshot ] | None :
211- """Handle agency catalog request."""
213+ ) -> PaginationResult :
214+ """Retrieve agency-filtered catalog entries using cursor pagination.
215+
216+ Args:
217+ firebase_manager: Firebase manager providing Firestore access.
218+ query_params: Request parameters containing agency name, limit,
219+ and optional pagination cursor.
220+
221+ Returns:
222+ PaginationResult:
223+ data: Documents for the current page.
224+ next_cursor: Cursor for the next page, or None if exhausted.
225+ has_more: Whether more results exist.
226+ """
227+ col = firebase_manager .firestore_client .collection ("schemes" )
228+
229+ # Apply base filters for filtering within catalog
230+ query = col .where (filter = FieldFilter ("agency" , "==" , query_params .agency_name ))
231+ print ("Query:" , query )
212232
213- ref = firebase_manager .firestore_client .collection ("schemes" ).where (
214- filter = FieldFilter ("agency" , "==" , query_params .agency_name )
233+ return get_paginated_results (
234+ collection_ref = col ,
235+ base_query = query ,
236+ cursor = query_params .cursor ,
237+ limit = query_params .limit ,
215238 )
216- return ref .get ()
0 commit comments