fastapi-toolsets 3.0.1__tar.gz → 3.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/PKG-INFO +1 -1
  2. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/pyproject.toml +1 -1
  3. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/__init__.py +1 -1
  4. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/crud/__init__.py +2 -0
  5. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/crud/factory.py +76 -11
  6. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/schemas.py +1 -0
  7. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/types.py +2 -1
  8. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/LICENSE +0 -0
  9. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/README.md +0 -0
  10. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/_imports.py +0 -0
  11. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/__init__.py +0 -0
  12. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/app.py +0 -0
  13. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
  14. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
  15. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/config.py +0 -0
  16. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/pyproject.py +0 -0
  17. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/utils.py +0 -0
  18. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/crud/search.py +0 -0
  19. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/db.py +0 -0
  20. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/dependencies.py +0 -0
  21. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/__init__.py +0 -0
  22. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
  23. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/handler.py +0 -0
  24. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
  25. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/enum.py +0 -0
  26. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/registry.py +0 -0
  27. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/utils.py +0 -0
  28. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/logger.py +0 -0
  29. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/metrics/__init__.py +0 -0
  30. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/metrics/handler.py +0 -0
  31. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/metrics/registry.py +0 -0
  32. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/models/__init__.py +0 -0
  33. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/models/columns.py +0 -0
  34. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/models/watched.py +0 -0
  35. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/py.typed +0 -0
  36. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/pytest/__init__.py +0 -0
  37. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/pytest/plugin.py +0 -0
  38. {fastapi_toolsets-3.0.1 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/pytest/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-toolsets
3
- Version: 3.0.1
3
+ Version: 3.0.2
4
4
  Summary: Production-ready utilities for FastAPI applications
5
5
  Keywords: fastapi,sqlalchemy,postgresql
6
6
  Author: d3vyce
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-toolsets"
3
- version = "3.0.1"
3
+ version = "3.0.2"
4
4
  description = "Production-ready utilities for FastAPI applications"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,4 +21,4 @@ Example usage:
21
21
  return Response(data={"user": user.username}, message="Success")
22
22
  """
23
23
 
24
- __version__ = "3.0.1"
24
+ __version__ = "3.0.2"
@@ -12,6 +12,7 @@ from ..types import (
12
12
  JoinType,
13
13
  M2MFieldType,
14
14
  OrderByClause,
15
+ OrderFieldType,
15
16
  SearchFieldType,
16
17
  )
17
18
  from .factory import AsyncCrud, CrudFactory
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "M2MFieldType",
29
30
  "NoSearchableFieldsError",
30
31
  "OrderByClause",
32
+ "OrderFieldType",
31
33
  "PaginationType",
32
34
  "SearchConfig",
33
35
  "SearchFieldType",
@@ -38,6 +38,7 @@ from ..types import (
38
38
  M2MFieldType,
39
39
  ModelType,
40
40
  OrderByClause,
41
+ OrderFieldType,
41
42
  SchemaType,
42
43
  SearchFieldType,
43
44
  )
@@ -134,7 +135,7 @@ class AsyncCrud(Generic[ModelType]):
134
135
  model: ClassVar[type[DeclarativeBase]]
135
136
  searchable_fields: ClassVar[Sequence[SearchFieldType] | None] = None
136
137
  facet_fields: ClassVar[Sequence[FacetFieldType] | None] = None
137
- order_fields: ClassVar[Sequence[QueryableAttribute[Any]] | None] = None
138
+ order_fields: ClassVar[Sequence[OrderFieldType] | None] = None
138
139
  m2m_fields: ClassVar[M2MFieldType | None] = None
139
140
  default_load_options: ClassVar[Sequence[ExecutableOption] | None] = None
140
141
  cursor_column: ClassVar[Any | None] = None
@@ -169,6 +170,18 @@ class AsyncCrud(Generic[ModelType]):
169
170
  return load_options
170
171
  return cls.default_load_options
171
172
 
173
+ @classmethod
174
+ async def _reload_with_options(
175
+ cls: type[Self], session: AsyncSession, instance: ModelType
176
+ ) -> ModelType:
177
+ """Re-query instance by PK with default_load_options applied."""
178
+ mapper = cls.model.__mapper__
179
+ pk_filters = [
180
+ getattr(cls.model, col.key) == getattr(instance, col.key)
181
+ for col in mapper.primary_key
182
+ ]
183
+ return await cls.get(session, filters=pk_filters)
184
+
172
185
  @classmethod
173
186
  async def _resolve_m2m(
174
187
  cls: type[Self],
@@ -278,6 +291,17 @@ class AsyncCrud(Generic[ModelType]):
278
291
  return None
279
292
  return search_field_keys(fields)
280
293
 
294
+ @classmethod
295
+ def _resolve_order_columns(
296
+ cls: type[Self],
297
+ order_fields: Sequence[OrderFieldType] | None,
298
+ ) -> list[str] | None:
299
+ """Return sort column keys, or None if no order fields configured."""
300
+ fields = order_fields if order_fields is not None else cls.order_fields
301
+ if not fields:
302
+ return None
303
+ return sorted(facet_keys(fields))
304
+
281
305
  @classmethod
282
306
  def _build_paginate_params(
283
307
  cls: type[Self],
@@ -290,7 +314,7 @@ class AsyncCrud(Generic[ModelType]):
290
314
  order: bool,
291
315
  search_fields: Sequence[SearchFieldType] | None,
292
316
  facet_fields: Sequence[FacetFieldType] | None,
293
- order_fields: Sequence[QueryableAttribute[Any]] | None,
317
+ order_fields: Sequence[OrderFieldType] | None,
294
318
  default_order_field: QueryableAttribute[Any] | None,
295
319
  default_order: Literal["asc", "desc"],
296
320
  ) -> Callable[..., Awaitable[dict[str, Any]]]:
@@ -349,14 +373,15 @@ class AsyncCrud(Generic[ModelType]):
349
373
  )
350
374
  reserved_names.update(filter_keys)
351
375
 
352
- order_field_map: dict[str, QueryableAttribute[Any]] | None = None
376
+ order_field_map: dict[str, OrderFieldType] | None = None
353
377
  order_valid_keys: list[str] | None = None
354
378
  if order:
355
379
  resolved_order = (
356
380
  order_fields if order_fields is not None else cls.order_fields
357
381
  )
358
382
  if resolved_order:
359
- order_field_map = {f.key: f for f in resolved_order}
383
+ keys = facet_keys(resolved_order)
384
+ order_field_map = dict(zip(keys, resolved_order))
360
385
  order_valid_keys = sorted(order_field_map.keys())
361
386
  all_params.extend(
362
387
  [
@@ -408,9 +433,16 @@ class AsyncCrud(Generic[ModelType]):
408
433
  else:
409
434
  field = order_field_map[order_by_val]
410
435
  if field is not None:
411
- result["order_by"] = (
412
- field.asc() if order_dir == "asc" else field.desc()
413
- )
436
+ if isinstance(field, tuple):
437
+ col = field[-1]
438
+ result["order_by"] = (
439
+ col.asc() if order_dir == "asc" else col.desc()
440
+ )
441
+ result["order_joins"] = list(field[:-1])
442
+ else:
443
+ result["order_by"] = (
444
+ field.asc() if order_dir == "asc" else field.desc()
445
+ )
414
446
  else:
415
447
  result["order_by"] = None
416
448
 
@@ -434,7 +466,7 @@ class AsyncCrud(Generic[ModelType]):
434
466
  order: bool = True,
435
467
  search_fields: Sequence[SearchFieldType] | None = None,
436
468
  facet_fields: Sequence[FacetFieldType] | None = None,
437
- order_fields: Sequence[QueryableAttribute[Any]] | None = None,
469
+ order_fields: Sequence[OrderFieldType] | None = None,
438
470
  default_order_field: QueryableAttribute[Any] | None = None,
439
471
  default_order: Literal["asc", "desc"] = "asc",
440
472
  ) -> Callable[..., Awaitable[dict[str, Any]]]:
@@ -496,7 +528,7 @@ class AsyncCrud(Generic[ModelType]):
496
528
  order: bool = True,
497
529
  search_fields: Sequence[SearchFieldType] | None = None,
498
530
  facet_fields: Sequence[FacetFieldType] | None = None,
499
- order_fields: Sequence[QueryableAttribute[Any]] | None = None,
531
+ order_fields: Sequence[OrderFieldType] | None = None,
500
532
  default_order_field: QueryableAttribute[Any] | None = None,
501
533
  default_order: Literal["asc", "desc"] = "asc",
502
534
  ) -> Callable[..., Awaitable[dict[str, Any]]]:
@@ -561,7 +593,7 @@ class AsyncCrud(Generic[ModelType]):
561
593
  order: bool = True,
562
594
  search_fields: Sequence[SearchFieldType] | None = None,
563
595
  facet_fields: Sequence[FacetFieldType] | None = None,
564
- order_fields: Sequence[QueryableAttribute[Any]] | None = None,
596
+ order_fields: Sequence[OrderFieldType] | None = None,
565
597
  default_order_field: QueryableAttribute[Any] | None = None,
566
598
  default_order: Literal["asc", "desc"] = "asc",
567
599
  ) -> Callable[..., Awaitable[dict[str, Any]]]:
@@ -685,6 +717,8 @@ class AsyncCrud(Generic[ModelType]):
685
717
 
686
718
  session.add(db_model)
687
719
  await session.refresh(db_model)
720
+ if cls.default_load_options:
721
+ db_model = await cls._reload_with_options(session, db_model)
688
722
  result = cast(ModelType, db_model)
689
723
  if schema:
690
724
  return Response(data=schema.model_validate(result))
@@ -1040,6 +1074,8 @@ class AsyncCrud(Generic[ModelType]):
1040
1074
  for rel_attr, related_instances in m2m_resolved.items():
1041
1075
  setattr(db_model, rel_attr, related_instances)
1042
1076
  await session.refresh(db_model)
1077
+ if cls.default_load_options:
1078
+ db_model = await cls._reload_with_options(session, db_model)
1043
1079
  if schema:
1044
1080
  return Response(data=schema.model_validate(db_model))
1045
1081
  return db_model
@@ -1202,12 +1238,14 @@ class AsyncCrud(Generic[ModelType]):
1202
1238
  outer_join: bool = False,
1203
1239
  load_options: Sequence[ExecutableOption] | None = None,
1204
1240
  order_by: OrderByClause | None = None,
1241
+ order_joins: list[Any] | None = None,
1205
1242
  page: int = 1,
1206
1243
  items_per_page: int = 20,
1207
1244
  include_total: bool = True,
1208
1245
  search: str | SearchConfig | None = None,
1209
1246
  search_fields: Sequence[SearchFieldType] | None = None,
1210
1247
  search_column: str | None = None,
1248
+ order_fields: Sequence[OrderFieldType] | None = None,
1211
1249
  facet_fields: Sequence[FacetFieldType] | None = None,
1212
1250
  filter_by: dict[str, Any] | BaseModel | None = None,
1213
1251
  schema: type[BaseModel],
@@ -1228,6 +1266,7 @@ class AsyncCrud(Generic[ModelType]):
1228
1266
  search: Search query string or SearchConfig object
1229
1267
  search_fields: Fields to search in (overrides class default)
1230
1268
  search_column: Restrict search to a single column key.
1269
+ order_fields: Fields allowed for sorting (overrides class default).
1231
1270
  facet_fields: Columns to compute distinct values for (overrides class default)
1232
1271
  filter_by: Dict of {column_key: value} to filter by declared facet fields.
1233
1272
  Keys must match the column.key of a facet field. Scalar → equality,
@@ -1264,6 +1303,10 @@ class AsyncCrud(Generic[ModelType]):
1264
1303
  # Apply search joins (always outer joins for search)
1265
1304
  q = _apply_search_joins(q, search_joins)
1266
1305
 
1306
+ # Apply order joins (relation joins required for order_by field)
1307
+ if order_joins:
1308
+ q = _apply_search_joins(q, order_joins)
1309
+
1267
1310
  if filters:
1268
1311
  q = q.where(and_(*filters))
1269
1312
  if resolved := cls._resolve_load_options(load_options):
@@ -1308,6 +1351,7 @@ class AsyncCrud(Generic[ModelType]):
1308
1351
  session, facet_fields, filters, search_joins
1309
1352
  )
1310
1353
  search_columns = cls._resolve_search_columns(search_fields)
1354
+ order_columns = cls._resolve_order_columns(order_fields)
1311
1355
 
1312
1356
  return OffsetPaginatedResponse(
1313
1357
  data=items,
@@ -1319,6 +1363,7 @@ class AsyncCrud(Generic[ModelType]):
1319
1363
  ),
1320
1364
  filter_attributes=filter_attributes,
1321
1365
  search_columns=search_columns,
1366
+ order_columns=order_columns,
1322
1367
  )
1323
1368
 
1324
1369
  @classmethod
@@ -1332,10 +1377,12 @@ class AsyncCrud(Generic[ModelType]):
1332
1377
  outer_join: bool = False,
1333
1378
  load_options: Sequence[ExecutableOption] | None = None,
1334
1379
  order_by: OrderByClause | None = None,
1380
+ order_joins: list[Any] | None = None,
1335
1381
  items_per_page: int = 20,
1336
1382
  search: str | SearchConfig | None = None,
1337
1383
  search_fields: Sequence[SearchFieldType] | None = None,
1338
1384
  search_column: str | None = None,
1385
+ order_fields: Sequence[OrderFieldType] | None = None,
1339
1386
  facet_fields: Sequence[FacetFieldType] | None = None,
1340
1387
  filter_by: dict[str, Any] | BaseModel | None = None,
1341
1388
  schema: type[BaseModel],
@@ -1357,6 +1404,7 @@ class AsyncCrud(Generic[ModelType]):
1357
1404
  search: Search query string or SearchConfig object.
1358
1405
  search_fields: Fields to search in (overrides class default).
1359
1406
  search_column: Restrict search to a single column key.
1407
+ order_fields: Fields allowed for sorting (overrides class default).
1360
1408
  facet_fields: Columns to compute distinct values for (overrides class default).
1361
1409
  filter_by: Dict of {column_key: value} to filter by declared facet fields.
1362
1410
  Keys must match the column.key of a facet field. Scalar → equality,
@@ -1410,6 +1458,10 @@ class AsyncCrud(Generic[ModelType]):
1410
1458
  # Apply search joins (always outer joins)
1411
1459
  q = _apply_search_joins(q, search_joins)
1412
1460
 
1461
+ # Apply order joins (relation joins required for order_by field)
1462
+ if order_joins:
1463
+ q = _apply_search_joins(q, order_joins)
1464
+
1413
1465
  if filters:
1414
1466
  q = q.where(and_(*filters))
1415
1467
  if resolved := cls._resolve_load_options(load_options):
@@ -1468,6 +1520,7 @@ class AsyncCrud(Generic[ModelType]):
1468
1520
  session, facet_fields, filters, search_joins
1469
1521
  )
1470
1522
  search_columns = cls._resolve_search_columns(search_fields)
1523
+ order_columns = cls._resolve_order_columns(order_fields)
1471
1524
 
1472
1525
  return CursorPaginatedResponse(
1473
1526
  data=items,
@@ -1479,6 +1532,7 @@ class AsyncCrud(Generic[ModelType]):
1479
1532
  ),
1480
1533
  filter_attributes=filter_attributes,
1481
1534
  search_columns=search_columns,
1535
+ order_columns=order_columns,
1482
1536
  )
1483
1537
 
1484
1538
  @overload
@@ -1493,6 +1547,7 @@ class AsyncCrud(Generic[ModelType]):
1493
1547
  outer_join: bool = ...,
1494
1548
  load_options: Sequence[ExecutableOption] | None = ...,
1495
1549
  order_by: OrderByClause | None = ...,
1550
+ order_joins: list[Any] | None = ...,
1496
1551
  page: int = ...,
1497
1552
  cursor: str | None = ...,
1498
1553
  items_per_page: int = ...,
@@ -1500,6 +1555,7 @@ class AsyncCrud(Generic[ModelType]):
1500
1555
  search: str | SearchConfig | None = ...,
1501
1556
  search_fields: Sequence[SearchFieldType] | None = ...,
1502
1557
  search_column: str | None = ...,
1558
+ order_fields: Sequence[OrderFieldType] | None = ...,
1503
1559
  facet_fields: Sequence[FacetFieldType] | None = ...,
1504
1560
  filter_by: dict[str, Any] | BaseModel | None = ...,
1505
1561
  schema: type[BaseModel],
@@ -1517,6 +1573,7 @@ class AsyncCrud(Generic[ModelType]):
1517
1573
  outer_join: bool = ...,
1518
1574
  load_options: Sequence[ExecutableOption] | None = ...,
1519
1575
  order_by: OrderByClause | None = ...,
1576
+ order_joins: list[Any] | None = ...,
1520
1577
  page: int = ...,
1521
1578
  cursor: str | None = ...,
1522
1579
  items_per_page: int = ...,
@@ -1524,6 +1581,7 @@ class AsyncCrud(Generic[ModelType]):
1524
1581
  search: str | SearchConfig | None = ...,
1525
1582
  search_fields: Sequence[SearchFieldType] | None = ...,
1526
1583
  search_column: str | None = ...,
1584
+ order_fields: Sequence[OrderFieldType] | None = ...,
1527
1585
  facet_fields: Sequence[FacetFieldType] | None = ...,
1528
1586
  filter_by: dict[str, Any] | BaseModel | None = ...,
1529
1587
  schema: type[BaseModel],
@@ -1540,6 +1598,7 @@ class AsyncCrud(Generic[ModelType]):
1540
1598
  outer_join: bool = False,
1541
1599
  load_options: Sequence[ExecutableOption] | None = None,
1542
1600
  order_by: OrderByClause | None = None,
1601
+ order_joins: list[Any] | None = None,
1543
1602
  page: int = 1,
1544
1603
  cursor: str | None = None,
1545
1604
  items_per_page: int = 20,
@@ -1547,6 +1606,7 @@ class AsyncCrud(Generic[ModelType]):
1547
1606
  search: str | SearchConfig | None = None,
1548
1607
  search_fields: Sequence[SearchFieldType] | None = None,
1549
1608
  search_column: str | None = None,
1609
+ order_fields: Sequence[OrderFieldType] | None = None,
1550
1610
  facet_fields: Sequence[FacetFieldType] | None = None,
1551
1611
  filter_by: dict[str, Any] | BaseModel | None = None,
1552
1612
  schema: type[BaseModel],
@@ -1575,6 +1635,7 @@ class AsyncCrud(Generic[ModelType]):
1575
1635
  search: Search query string or :class:`.SearchConfig` object.
1576
1636
  search_fields: Fields to search in (overrides class default).
1577
1637
  search_column: Restrict search to a single column key.
1638
+ order_fields: Fields allowed for sorting (overrides class default).
1578
1639
  facet_fields: Columns to compute distinct values for (overrides
1579
1640
  class default).
1580
1641
  filter_by: Dict of ``{column_key: value}`` to filter by declared
@@ -1600,10 +1661,12 @@ class AsyncCrud(Generic[ModelType]):
1600
1661
  outer_join=outer_join,
1601
1662
  load_options=load_options,
1602
1663
  order_by=order_by,
1664
+ order_joins=order_joins,
1603
1665
  items_per_page=items_per_page,
1604
1666
  search=search,
1605
1667
  search_fields=search_fields,
1606
1668
  search_column=search_column,
1669
+ order_fields=order_fields,
1607
1670
  facet_fields=facet_fields,
1608
1671
  filter_by=filter_by,
1609
1672
  schema=schema,
@@ -1618,12 +1681,14 @@ class AsyncCrud(Generic[ModelType]):
1618
1681
  outer_join=outer_join,
1619
1682
  load_options=load_options,
1620
1683
  order_by=order_by,
1684
+ order_joins=order_joins,
1621
1685
  page=page,
1622
1686
  items_per_page=items_per_page,
1623
1687
  include_total=include_total,
1624
1688
  search=search,
1625
1689
  search_fields=search_fields,
1626
1690
  search_column=search_column,
1691
+ order_fields=order_fields,
1627
1692
  facet_fields=facet_fields,
1628
1693
  filter_by=filter_by,
1629
1694
  schema=schema,
@@ -1638,7 +1703,7 @@ def CrudFactory(
1638
1703
  base_class: type[AsyncCrud[Any]] = AsyncCrud,
1639
1704
  searchable_fields: Sequence[SearchFieldType] | None = None,
1640
1705
  facet_fields: Sequence[FacetFieldType] | None = None,
1641
- order_fields: Sequence[QueryableAttribute[Any]] | None = None,
1706
+ order_fields: Sequence[OrderFieldType] | None = None,
1642
1707
  m2m_fields: M2MFieldType | None = None,
1643
1708
  default_load_options: Sequence[ExecutableOption] | None = None,
1644
1709
  cursor_column: Any | None = None,
@@ -163,6 +163,7 @@ class PaginatedResponse(BaseResponse, Generic[DataT]):
163
163
  pagination_type: PaginationType | None = None
164
164
  filter_attributes: dict[str, list[Any]] | None = None
165
165
  search_columns: list[str] | None = None
166
+ order_columns: list[str] | None = None
166
167
 
167
168
  _discriminated_union_cache: ClassVar[dict[Any, Any]] = {}
168
169
 
@@ -19,9 +19,10 @@ JoinType = list[tuple[type[DeclarativeBase] | Any, Any]]
19
19
  M2MFieldType = Mapping[str, QueryableAttribute[Any]]
20
20
  OrderByClause = ColumnElement[Any] | QueryableAttribute[Any]
21
21
 
22
- # Search / facet type aliases
22
+ # Search / facet / order type aliases
23
23
  SearchFieldType = InstrumentedAttribute[Any] | tuple[InstrumentedAttribute[Any], ...]
24
24
  FacetFieldType = SearchFieldType
25
+ OrderFieldType = SearchFieldType
25
26
 
26
27
  # Dependency type aliases
27
28
  SessionDependency = Callable[[], AsyncGenerator[AsyncSession, None]] | Any