1414from pydantic import Json
1515
1616from ..security import check , permissions_as_dict
17- from ..types import ComponentState , InputState , fk
17+ from ..types import ComponentState , InputState , fk , resources_key
1818
1919if sys .version_info >= (3 , 10 ):
2020 from typing import TypeAlias
2626else :
2727 from typing_extensions import TypedDict
2828
29- _ID = TypeVar ("_ID" )
29+ _ID = TypeVar ("_ID" , bound = tuple [ object , ...] )
3030Record = dict [str , object ]
3131Meta = Optional [dict [str , object ]]
3232
@@ -87,6 +87,22 @@ class GetManyParams(_Params):
8787 ids : Json [tuple [str , ...]]
8888
8989
90+ class GetManyRefAPIParams (_Params ):
91+ target : str
92+ id : str
93+ pagination : Json [_Pagination ]
94+ sort : Json [_Sort ]
95+ filter : Json [dict [str , object ]]
96+
97+
98+ class GetManyRefParams (_Params ):
99+ target : tuple [str , ...]
100+ id : tuple [object , ...]
101+ pagination : Json [_Pagination ]
102+ sort : Json [_Sort ]
103+ filter : Json [dict [str , object ]]
104+
105+
90106class _CreateData (TypedDict ):
91107 """Id will not be included for create calls."""
92108 data : Record
@@ -116,6 +132,11 @@ class DeleteManyParams(_Params):
116132 ids : Json [tuple [str , ...]]
117133
118134
135+ class _ListQuery (TypedDict ):
136+ sort : _Sort
137+ filter : dict [str , object ]
138+
139+
119140class AbstractAdminResource (ABC , Generic [_ID ]):
120141 name : str
121142 fields : dict [str , ComponentState ]
@@ -155,6 +176,10 @@ async def get_one(self, record_id: _ID, meta: Meta) -> Record:
155176 async def get_many (self , record_ids : Sequence [_ID ], meta : Meta ) -> list [Record ]:
156177 """Return the matching records."""
157178
179+ @abstractmethod
180+ async def get_many_ref (self , params : GetManyRefParams ) -> tuple [list [Record ], int ]:
181+ """Return list of records and total count available (when not paginating)."""
182+
158183 @abstractmethod
159184 async def update (self , record_id : _ID , data : Record , previous_data : Record ,
160185 meta : Meta ) -> Record :
@@ -176,38 +201,30 @@ async def delete(self, record_id: _ID, previous_data: Record, meta: Meta) -> Rec
176201 async def delete_many (self , record_ids : Sequence [_ID ], meta : Meta ) -> list [_ID ]:
177202 """Delete the matching records and return their IDs."""
178203
204+ async def get_many_ref_name (self , target : str , meta : Meta ) -> str :
205+ """Return the resource name for the reference.
206+
207+ This can be used to change which resource should be returned by get_many_ref().
208+
209+ For example, if we have an SQLAlchemy model called 'parent' with a relationship
210+ called children, then a normal get_many_ref_name() call would go to the 'child'
211+ model with the details from the parent, and the default behaviour would work.
212+
213+ However, the SQLAlchemy backend uses the meta to switch this and send the request
214+ to the 'parent' model instead and then use the children ORM attribute to fetch
215+ the referenced resources, thus requiring this method to return 'child'.
216+ This allows the SQLAlchemy backend to support complex relationships (e.g.
217+ many-to-many) without needing react-admin to know the details.
218+ """
219+ return self .name
220+
179221 # https://marmelab.com/react-admin/DataProviderWriting.html
180222
181223 @final
182224 async def _get_list (self , request : web .Request ) -> web .Response :
183225 await check_permission (request , f"admin.{ self .name } .view" , context = (request , None ))
184226 query = check (GetListParams , request .query )
185-
186- # When sort order refers to "id", this should be translated to primary key.
187- if query ["sort" ]["field" ] == "id" :
188- query ["sort" ]["field" ] = self .primary_key [0 ]
189- else :
190- query ["sort" ]["field" ] = query ["sort" ]["field" ].removeprefix ("data." )
191-
192- query ["filter" ].update (check (dict [str , object ], query ["filter" ].pop ("data" , {}))) # type: ignore[type-var]
193-
194- merged_filter = {}
195- for k , v in query ["filter" ].items ():
196- if k .startswith ("fk_" ):
197- v = check (str , v )
198- for c , cv in zip (k .removeprefix ("fk_" ).split ("__" ), v .split ("|" )):
199- merged_filter [c ] = check (self ._raw_record_type [c ], cv )
200- else :
201- merged_filter [k ] = check (self ._raw_record_type [k ], v )
202- query ["filter" ] = merged_filter
203-
204- # Add filters from advanced permissions.
205- # The permissions will be cached on the request from a previous permissions check.
206- permissions = permissions_as_dict (request ["aiohttpadmin_permissions" ])
207- filters = permissions .get (f"admin.{ self .name } .view" ,
208- permissions .get (f"admin.{ self .name } .*" , {}))
209- for k , v in filters .items ():
210- query ["filter" ][k ] = v
227+ self ._process_list_query (query , request )
211228
212229 raw_results , total = await self .get_list (query )
213230 results = [await self ._convert_record (r , request ) for r in raw_results
@@ -239,6 +256,33 @@ async def _get_many(self, request: web.Request) -> web.Response:
239256 if await permits (request , f"admin.{ self .name } .view" , context = (request , r ))]
240257 return json_response ({"data" : results })
241258
259+ @final
260+ async def _get_many_ref (self , request : web .Request ) -> web .Response :
261+ query = check (GetManyRefAPIParams , request .query )
262+ meta = query ["filter" ].pop ("__meta__" , None )
263+ if meta is not None :
264+ query ["meta" ] = check (dict [str , object ], meta )
265+ reference = await self .get_many_ref_name (query ["target" ], query .get ("meta" ))
266+ ref_model = request .app [resources_key ][reference ]
267+
268+ await check_permission (request , f"admin.{ ref_model .name } .view" , context = (request , None ))
269+
270+ ref_model ._process_list_query (query , request )
271+
272+ if query ["target" ].startswith ("fk_" ):
273+ target = tuple (query ["target" ].removeprefix ("fk_" ).split ("__" ))
274+ record_id = tuple (check (self ._raw_record_type [k ], v )
275+ for k , v in zip (target , query ["id" ].split ("|" )))
276+ else :
277+ target = (query ["target" ],)
278+ record_id = check (self ._id_type , query ["id" ].split ("|" ))
279+
280+ raw_results , total = await self .get_many_ref ({** query , "target" : target , "id" : record_id })
281+
282+ results = [await ref_model ._convert_record (r , request ) for r in raw_results
283+ if await permits (request , f"admin.{ ref_model .name } .view" , context = (request , r ))]
284+ return json_response ({"data" : results , "total" : total })
285+
242286 @final
243287 async def _create (self , request : web .Request ) -> web .Response :
244288 query = check (CreateParams , request .query )
@@ -350,7 +394,7 @@ async def _delete_many(self, request: web.Request) -> web.Response:
350394 @final
351395 def _check_record (self , record : Record ) -> Record :
352396 """Check and convert input record."""
353- return check (self ._record_type , record ) # type: ignore[no-any-return]
397+ return check (self ._record_type , record )
354398
355399 @final
356400 async def _convert_record (self , record : Record , request : web .Request ) -> APIRecord :
@@ -371,6 +415,33 @@ def _convert_ids(self, ids: Sequence[_ID]) -> tuple[str, ...]:
371415 """Convert IDs to correct output format."""
372416 return tuple (str (i ) for i in ids )
373417
418+ def _process_list_query (self , query : _ListQuery , request : web .Request ) -> None :
419+ # When sort order refers to "id", this should be translated to primary key.
420+ if query ["sort" ]["field" ] == "id" :
421+ query ["sort" ]["field" ] = self .primary_key [0 ]
422+ else :
423+ query ["sort" ]["field" ] = query ["sort" ]["field" ].removeprefix ("data." )
424+
425+ query ["filter" ].update (check (dict [str , object ], query ["filter" ].pop ("data" , {})))
426+
427+ merged_filter = {}
428+ for k , v in query ["filter" ].items ():
429+ if k .startswith ("fk_" ):
430+ v = check (str , v )
431+ for c , cv in zip (k .removeprefix ("fk_" ).split ("__" ), v .split ("|" )):
432+ merged_filter [c ] = check (self ._raw_record_type [c ], cv )
433+ else :
434+ merged_filter [k ] = check (self ._raw_record_type [k ], v )
435+ query ["filter" ] = merged_filter
436+
437+ # Add filters from advanced permissions.
438+ # The permissions will be cached on the request from a previous permissions check.
439+ permissions = permissions_as_dict (request ["aiohttpadmin_permissions" ])
440+ filters = permissions .get (f"admin.{ self .name } .view" ,
441+ permissions .get (f"admin.{ self .name } .*" , {}))
442+ for k , v in filters .items ():
443+ query ["filter" ][k ] = v
444+
374445 @cached_property
375446 def routes (self ) -> tuple [web .RouteDef , ...]:
376447 """Routes to act on this resource.
@@ -382,6 +453,7 @@ def routes(self) -> tuple[web.RouteDef, ...]:
382453 web .get (url + "/list" , self ._get_list , name = self .name + "_get_list" ),
383454 web .get (url + "/one" , self ._get_one , name = self .name + "_get_one" ),
384455 web .get (url , self ._get_many , name = self .name + "_get_many" ),
456+ web .get (url + "/ref" , self ._get_many_ref , name = self .name + "_get_many_ref" ),
385457 web .post (url , self ._create , name = self .name + "_create" ),
386458 web .put (url + "/update" , self ._update , name = self .name + "_update" ),
387459 web .put (url + "/update_many" , self ._update_many , name = self .name + "_update_many" ),
0 commit comments