@apso/cli 0.35.0 → 0.37.0

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.
@@ -13,6 +13,9 @@ type ParsedFilter struct {
13
13
  type ParsedSort struct {
14
14
  Field string
15
15
  Direction string // "ASC" or "DESC"
16
+ // Nulls is the optional PostgREST null-ordering hint: "" (default),
17
+ // "NULLS FIRST", or "NULLS LAST". Only populated by the PostgREST dialect.
18
+ Nulls string
16
19
  }
17
20
 
18
21
  // ParsedJoin represents a relation to eager-load.
@@ -21,6 +24,28 @@ type ParsedJoin struct {
21
24
  Fields []string // nil means load all fields
22
25
  }
23
26
 
27
+ // FilterGroup is a nested boolean combination of conditions, used by the
28
+ // PostgREST dialect to model ?or=(...) / ?and=(...) logical combinators
29
+ // (apso #56). Conjunction is "OR" or "AND". A branch is either a leaf
30
+ // ParsedFilter or a nested FilterGroup; exactly one is set per branch.
31
+ type FilterGroup struct {
32
+ Conjunction string // "OR" or "AND"
33
+ Filters []ParsedFilter
34
+ Groups []FilterGroup
35
+ }
36
+
37
+ // Dialect identifies which query-param convention a request used.
38
+ type Dialect string
39
+
40
+ const (
41
+ // DialectNestjsx is the incumbent nestjsx/crud dialect
42
+ // (fields/filter/join/sort/s/or/per_page).
43
+ DialectNestjsx Dialect = "nestjsx"
44
+ // DialectPostgrest is the PostgREST/Supabase dialect
45
+ // (select/order, col=op.value, or=()/and=()).
46
+ DialectPostgrest Dialect = "postgrest"
47
+ )
48
+
24
49
  // QueryOptions holds the parsed query parameters for list endpoints.
25
50
  type QueryOptions struct {
26
51
  Filters []ParsedFilter
@@ -30,6 +55,24 @@ type QueryOptions struct {
30
55
  Joins []ParsedJoin
31
56
  Limit int
32
57
  Page int
58
+
59
+ // Dialect is the detected request dialect. Defaults to DialectNestjsx so
60
+ // existing (nestjsx) requests are byte-identical.
61
+ Dialect Dialect
62
+
63
+ // Groups holds PostgREST ?or=()/?and=() logical combinators (apso #56).
64
+ // Each group is ANDed with the flat Filters. Only used by the PostgREST
65
+ // dialect.
66
+ Groups []FilterGroup
67
+
68
+ // Offset is the PostgREST ?offset= row skip. -1 means "not supplied".
69
+ // The nestjsx dialect derives its offset from Page instead.
70
+ Offset int
71
+
72
+ // Bare requests a bare JSON array response (no {data,count,...} envelope).
73
+ // The PostgREST dialect always returns a bare array (apso #58); nestjsx
74
+ // keeps its envelope.
75
+ Bare bool
33
76
  }
34
77
 
35
78
  // PaginatedResult is the standard paginated response envelope.
@@ -44,7 +87,9 @@ type PaginatedResult struct {
44
87
  // DefaultQueryOptions returns options with default pagination values.
45
88
  func DefaultQueryOptions() QueryOptions {
46
89
  return QueryOptions{
47
- Limit: 25,
48
- Page: 1,
90
+ Limit: 25,
91
+ Page: 1,
92
+ Dialect: DialectNestjsx,
93
+ Offset: -1,
49
94
  }
50
95
  }
@@ -1,7 +1,9 @@
1
1
  <%~ includeFile('../header.eta', it) %>
2
2
 
3
- from typing import List, Optional
4
- from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
3
+ from typing import List, Optional, Union
4
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
5
+ from fastapi.encoders import jsonable_encoder
6
+ from fastapi.responses import JSONResponse
5
7
  from sqlalchemy.ext.asyncio import AsyncSession
6
8
 
7
9
  from app.database import get_db
@@ -14,7 +16,7 @@ from ..schemas.<%= it.entityName.toLowerCase() %> import (
14
16
  <%= it.entityName %>List,
15
17
  )
16
18
  from ..services.<%= it.entityName.toLowerCase() %> import <%= it.entityName %>Service
17
- from ..utils.query import parse_query_params
19
+ from ..utils.query import parse_query_params, QueryParseError
18
20
 
19
21
  router = APIRouter(
20
22
  prefix="/<%= it.pluralEntityName.toLowerCase() %>",
@@ -24,27 +26,67 @@ router = APIRouter(
24
26
 
25
27
  @router.get(
26
28
  "",
27
- response_model=<%= it.entityName %>List,
28
29
  summary="Retrieve multiple <%= it.pluralEntityName %>",
30
+ response_model=Union[<%= it.entityName %>List, List[<%= it.entityName %>Schema]],
29
31
  )
30
32
  async def get_many(
31
33
  request: Request,
32
34
  db: AsyncSession = Depends(get_db),
33
- ) -> <%= it.entityName %>List:
35
+ x_crud_dialect: Optional[str] = Header(default=None, alias="X-Crud-Dialect"),
36
+ ):
34
37
  """
35
- Retrieve a paginated list of <%= it.pluralEntityName %>.
38
+ Retrieve <%= it.pluralEntityName %> under either query dialect.
36
39
 
37
- Supports query parameters:
40
+ Dialect is chosen by the `X-Crud-Dialect` header (postgrest|nestjsx), else
41
+ inferred from the param shape (parity with the TypeScript services).
42
+
43
+ nestjsx (default) — envelope response {data,count,total,page,pageCount}:
38
44
  - filter=field||$operator||value (repeatable)
39
45
  - or=field||$operator||value (repeatable)
40
46
  - sort=field,ASC (repeatable)
41
47
  - fields=field1,field2
42
48
  - join=relation (repeatable)
43
49
  - limit=25&page=1
50
+
51
+ postgrest — bare JSON array response:
52
+ - select=col,alias:col,rel(cols)
53
+ - col=op.value filters (eq/neq/gt/gte/lt/lte/like/ilike/in/is, not.<op>)
54
+ - or=(...) / and=(...) logical groups
55
+ - order=col.asc|desc[.nullsfirst|nullslast]
56
+ - limit=..&offset=.. (windows the bare array)
44
57
  """
45
58
  service = <%= it.entityName %>Service(db)
46
- options = parse_query_params(request.query_params)
47
- result = await service.get_many(options)
59
+ try:
60
+ options = parse_query_params(request.query_params, x_crud_dialect)
61
+ except QueryParseError as exc:
62
+ raise HTTPException(
63
+ status_code=status.HTTP_400_BAD_REQUEST,
64
+ detail=str(exc),
65
+ )
66
+
67
+ if options.dialect == "postgrest":
68
+ # PostgREST always returns a bare JSON array (never the envelope), even
69
+ # when limit/offset are present.
70
+ try:
71
+ rows = await service.get_many_list(options)
72
+ except QueryParseError as exc:
73
+ raise HTTPException(
74
+ status_code=status.HTTP_400_BAD_REQUEST,
75
+ detail=str(exc),
76
+ )
77
+ payload = [
78
+ r if isinstance(r, dict) else <%= it.entityName %>Schema.model_validate(r)
79
+ for r in rows
80
+ ]
81
+ return JSONResponse(content=jsonable_encoder(payload))
82
+
83
+ try:
84
+ result = await service.get_many(options)
85
+ except QueryParseError as exc:
86
+ raise HTTPException(
87
+ status_code=status.HTTP_400_BAD_REQUEST,
88
+ detail=str(exc),
89
+ )
48
90
 
49
91
  return <%= it.entityName %>List(
50
92
  data=result.data,
@@ -21,9 +21,16 @@ class <%= it.entityName %>Service:
21
21
  async def get_many(self, options: QueryOptions) -> PaginatedResult:
22
22
  """
23
23
  Retrieve a paginated list of <%= it.pluralEntityName %> with filtering, sorting, and joins.
24
+ (nestjsx dialect — {data,count,total,page,pageCount} envelope.)
24
25
  """
25
26
  return await self._qb.get_many(options)
26
27
 
28
+ async def get_many_list(self, options: QueryOptions) -> List[Any]:
29
+ """
30
+ Retrieve <%= it.pluralEntityName %> as a bare list (PostgREST dialect).
31
+ """
32
+ return await self._qb.get_many_list(options)
33
+
27
34
  async def get_one(
28
35
  self,
29
36
  id: Any,
@@ -2,8 +2,13 @@
2
2
 
3
3
  from .query import (
4
4
  parse_query_params,
5
+ parse_nestjsx_params,
6
+ parse_postgrest_params,
7
+ detect_dialect,
5
8
  QueryBuilder,
6
9
  QueryOptions,
10
+ QueryGroup,
11
+ QueryParseError,
7
12
  PaginatedResult,
8
13
  ParsedFilter,
9
14
  ParsedSort,
@@ -12,8 +17,13 @@ from .query import (
12
17
 
13
18
  __all__ = [
14
19
  "parse_query_params",
20
+ "parse_nestjsx_params",
21
+ "parse_postgrest_params",
22
+ "detect_dialect",
15
23
  "QueryBuilder",
16
24
  "QueryOptions",
25
+ "QueryGroup",
26
+ "QueryParseError",
17
27
  "PaginatedResult",
18
28
  "ParsedFilter",
19
29
  "ParsedSort",