@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.
- package/dist/lib/templates/go/handlers/handler.eta +35 -1
- package/dist/lib/templates/go/services/service.eta +33 -1
- package/dist/lib/templates/go/utils/query.eta +664 -10
- package/dist/lib/templates/go/utils/types.eta +47 -2
- package/dist/lib/templates/python/routers/router.eta +51 -9
- package/dist/lib/templates/python/services/service.eta +7 -0
- package/dist/lib/templates/python/utils/init.eta +10 -0
- package/dist/lib/templates/python/utils/query.eta +613 -60
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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:
|
|
48
|
-
Page:
|
|
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
|
-
|
|
35
|
+
x_crud_dialect: Optional[str] = Header(default=None, alias="X-Crud-Dialect"),
|
|
36
|
+
):
|
|
34
37
|
"""
|
|
35
|
-
Retrieve
|
|
38
|
+
Retrieve <%= it.pluralEntityName %> under either query dialect.
|
|
36
39
|
|
|
37
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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",
|