@apso/cli 0.11.0 → 0.12.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/LICENSE +191 -21
- package/dist/commands/generate.js +7 -0
- package/dist/lib/generators/base.d.ts +5 -0
- package/dist/lib/generators/base.js +7 -0
- package/dist/lib/generators/go.d.ts +1 -0
- package/dist/lib/generators/go.js +14 -0
- package/dist/lib/generators/python.d.ts +1 -0
- package/dist/lib/generators/python.js +8 -0
- package/dist/lib/templates/go/dto/dto.eta +39 -39
- package/dist/lib/templates/go/handlers/handler.eta +13 -15
- package/dist/lib/templates/go/index-module.eta +2 -2
- package/dist/lib/templates/go/models/model.eta +1 -2
- package/dist/lib/templates/go/services/service.eta +33 -23
- package/dist/lib/templates/go/utils/query.eta +301 -0
- package/dist/lib/templates/go/utils/types.eta +50 -0
- package/dist/lib/templates/python/routers/router.eta +24 -44
- package/dist/lib/templates/python/schemas/schema.eta +2 -2
- package/dist/lib/templates/python/services/service.eta +8 -63
- package/dist/lib/templates/python/utils/init.eta +21 -0
- package/dist/lib/templates/python/utils/query.eta +320 -0
- package/dist/lib/types/generator.d.ts +4 -0
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
<%~ includeFile('../../header.eta', it) %>
|
|
2
|
+
|
|
3
|
+
package utils
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"fmt"
|
|
7
|
+
"math"
|
|
8
|
+
"net/url"
|
|
9
|
+
"strconv"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
"gorm.io/gorm"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const (
|
|
16
|
+
MaxLimit = 200
|
|
17
|
+
DefaultLimit = 25
|
|
18
|
+
DefaultPage = 1
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// ParseQueryParams parses standard query parameters from a url.Values map
|
|
22
|
+
// into a QueryOptions struct.
|
|
23
|
+
func ParseQueryParams(params url.Values) QueryOptions {
|
|
24
|
+
opts := DefaultQueryOptions()
|
|
25
|
+
|
|
26
|
+
// Filters: ?filter=field||$op||value (repeatable)
|
|
27
|
+
for _, raw := range params["filter"] {
|
|
28
|
+
if f := parseFilterParam(raw); f != nil {
|
|
29
|
+
opts.Filters = append(opts.Filters, *f)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// OR filters: ?or=field||$op||value (repeatable)
|
|
34
|
+
for _, raw := range params["or"] {
|
|
35
|
+
if f := parseFilterParam(raw); f != nil {
|
|
36
|
+
opts.OrFilters = append(opts.OrFilters, *f)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Sorts: ?sort=field,ASC (repeatable)
|
|
41
|
+
for _, raw := range params["sort"] {
|
|
42
|
+
if s := parseSortParam(raw); s != nil {
|
|
43
|
+
opts.Sorts = append(opts.Sorts, *s)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fields: ?fields=field1,field2
|
|
48
|
+
if fieldsRaw := params.Get("fields"); fieldsRaw != "" {
|
|
49
|
+
for _, f := range strings.Split(fieldsRaw, ",") {
|
|
50
|
+
f = strings.TrimSpace(f)
|
|
51
|
+
if f != "" {
|
|
52
|
+
opts.Fields = append(opts.Fields, f)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Joins: ?join=relation or ?join=relation||field1,field2 (repeatable)
|
|
58
|
+
for _, raw := range params["join"] {
|
|
59
|
+
opts.Joins = append(opts.Joins, parseJoinParam(raw))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pagination
|
|
63
|
+
if limitRaw := params.Get("limit"); limitRaw != "" {
|
|
64
|
+
if v, err := strconv.Atoi(limitRaw); err == nil {
|
|
65
|
+
if v > MaxLimit {
|
|
66
|
+
v = MaxLimit
|
|
67
|
+
}
|
|
68
|
+
if v > 0 {
|
|
69
|
+
opts.Limit = v
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if pageRaw := params.Get("page"); pageRaw != "" {
|
|
75
|
+
if v, err := strconv.Atoi(pageRaw); err == nil && v > 0 {
|
|
76
|
+
opts.Page = v
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return opts
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ApplyQuery applies filters, sorts, and pagination from QueryOptions to a
|
|
84
|
+
// GORM query. The allowedFields map restricts which column names can be used
|
|
85
|
+
// in filters and sorts to prevent injection.
|
|
86
|
+
func ApplyQuery(db *gorm.DB, opts QueryOptions, allowedFields map[string]bool) (*gorm.DB, int64, int) {
|
|
87
|
+
query := db
|
|
88
|
+
|
|
89
|
+
// AND filters
|
|
90
|
+
for _, f := range opts.Filters {
|
|
91
|
+
if !allowedFields[f.Field] {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
query = applyOperator(query, f.Field, f.Operator, f.Value, false)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// OR filters
|
|
98
|
+
if len(opts.OrFilters) > 0 {
|
|
99
|
+
query = query.Where(func(tx *gorm.DB) *gorm.DB {
|
|
100
|
+
first := true
|
|
101
|
+
for _, f := range opts.OrFilters {
|
|
102
|
+
if !allowedFields[f.Field] {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if first {
|
|
106
|
+
tx = applyOperator(tx, f.Field, f.Operator, f.Value, false)
|
|
107
|
+
first = false
|
|
108
|
+
} else {
|
|
109
|
+
tx = applyOperator(tx, f.Field, f.Operator, f.Value, true)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return tx
|
|
113
|
+
}(db.Session(&gorm.Session{NewDB: true})))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Count before pagination
|
|
117
|
+
var total int64
|
|
118
|
+
countQuery := query.Session(&gorm.Session{})
|
|
119
|
+
countQuery.Count(&total)
|
|
120
|
+
|
|
121
|
+
// Sorting
|
|
122
|
+
for _, s := range opts.Sorts {
|
|
123
|
+
if !allowedFields[s.Field] {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
dir := "ASC"
|
|
127
|
+
if strings.EqualFold(s.Direction, "DESC") {
|
|
128
|
+
dir = "DESC"
|
|
129
|
+
}
|
|
130
|
+
query = query.Order(fmt.Sprintf("%s %s", s.Field, dir))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Field selection
|
|
134
|
+
if len(opts.Fields) > 0 {
|
|
135
|
+
var safe []string
|
|
136
|
+
for _, f := range opts.Fields {
|
|
137
|
+
if allowedFields[f] {
|
|
138
|
+
safe = append(safe, f)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if len(safe) > 0 {
|
|
142
|
+
query = query.Select(safe)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Joins (Preloads)
|
|
147
|
+
for _, j := range opts.Joins {
|
|
148
|
+
query = query.Preload(j.Relation)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Pagination
|
|
152
|
+
offset := (opts.Page - 1) * opts.Limit
|
|
153
|
+
query = query.Offset(offset).Limit(opts.Limit)
|
|
154
|
+
|
|
155
|
+
pageCount := int(math.Ceil(float64(total) / float64(opts.Limit)))
|
|
156
|
+
if pageCount < 1 {
|
|
157
|
+
pageCount = 1
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return query, total, pageCount
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Internal parsers
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
func parseFilterParam(raw string) *ParsedFilter {
|
|
168
|
+
parts := strings.SplitN(raw, "||", 3)
|
|
169
|
+
if len(parts) < 2 {
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
value := ""
|
|
173
|
+
if len(parts) > 2 {
|
|
174
|
+
value = parts[2]
|
|
175
|
+
}
|
|
176
|
+
return &ParsedFilter{
|
|
177
|
+
Field: parts[0],
|
|
178
|
+
Operator: parts[1],
|
|
179
|
+
Value: value,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func parseSortParam(raw string) *ParsedSort {
|
|
184
|
+
parts := strings.SplitN(raw, ",", 2)
|
|
185
|
+
if len(parts) == 0 || parts[0] == "" {
|
|
186
|
+
return nil
|
|
187
|
+
}
|
|
188
|
+
dir := "ASC"
|
|
189
|
+
if len(parts) > 1 {
|
|
190
|
+
d := strings.ToUpper(strings.TrimSpace(parts[1]))
|
|
191
|
+
if d == "DESC" {
|
|
192
|
+
dir = "DESC"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return &ParsedSort{
|
|
196
|
+
Field: parts[0],
|
|
197
|
+
Direction: dir,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func parseJoinParam(raw string) ParsedJoin {
|
|
202
|
+
parts := strings.SplitN(raw, "||", 2)
|
|
203
|
+
j := ParsedJoin{Relation: parts[0]}
|
|
204
|
+
if len(parts) > 1 && parts[1] != "" {
|
|
205
|
+
for _, f := range strings.Split(parts[1], ",") {
|
|
206
|
+
f = strings.TrimSpace(f)
|
|
207
|
+
if f != "" {
|
|
208
|
+
j.Fields = append(j.Fields, f)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return j
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Operator application
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
func applyOperator(query *gorm.DB, field, op, value string, useOr bool) *gorm.DB {
|
|
220
|
+
where := query.Where
|
|
221
|
+
if useOr {
|
|
222
|
+
where = query.Or
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
switch op {
|
|
226
|
+
case "$eq":
|
|
227
|
+
return where(fmt.Sprintf("%s = ?", field), value)
|
|
228
|
+
case "$ne":
|
|
229
|
+
return where(fmt.Sprintf("%s != ?", field), value)
|
|
230
|
+
case "$gt":
|
|
231
|
+
return where(fmt.Sprintf("%s > ?", field), value)
|
|
232
|
+
case "$gte":
|
|
233
|
+
return where(fmt.Sprintf("%s >= ?", field), value)
|
|
234
|
+
case "$lt":
|
|
235
|
+
return where(fmt.Sprintf("%s < ?", field), value)
|
|
236
|
+
case "$lte":
|
|
237
|
+
return where(fmt.Sprintf("%s <= ?", field), value)
|
|
238
|
+
case "$starts":
|
|
239
|
+
return where(fmt.Sprintf("%s LIKE ?", field), value+"%")
|
|
240
|
+
case "$ends":
|
|
241
|
+
return where(fmt.Sprintf("%s LIKE ?", field), "%"+value)
|
|
242
|
+
case "$cont":
|
|
243
|
+
return where(fmt.Sprintf("%s LIKE ?", field), "%"+value+"%")
|
|
244
|
+
case "$excl":
|
|
245
|
+
return where(fmt.Sprintf("%s NOT LIKE ?", field), "%"+value+"%")
|
|
246
|
+
case "$in":
|
|
247
|
+
return where(fmt.Sprintf("%s IN ?", field), splitValue(value))
|
|
248
|
+
case "$notin":
|
|
249
|
+
return where(fmt.Sprintf("%s NOT IN ?", field), splitValue(value))
|
|
250
|
+
case "$isnull":
|
|
251
|
+
return where(fmt.Sprintf("%s IS NULL", field))
|
|
252
|
+
case "$notnull":
|
|
253
|
+
return where(fmt.Sprintf("%s IS NOT NULL", field))
|
|
254
|
+
case "$between":
|
|
255
|
+
vals := splitValue(value)
|
|
256
|
+
if len(vals) >= 2 {
|
|
257
|
+
return where(fmt.Sprintf("%s BETWEEN ? AND ?", field), vals[0], vals[1])
|
|
258
|
+
}
|
|
259
|
+
return query
|
|
260
|
+
case "$eqL":
|
|
261
|
+
return where(fmt.Sprintf("LOWER(%s) = ?", field), strings.ToLower(value))
|
|
262
|
+
case "$neL":
|
|
263
|
+
return where(fmt.Sprintf("LOWER(%s) != ?", field), strings.ToLower(value))
|
|
264
|
+
case "$startsL":
|
|
265
|
+
return where(fmt.Sprintf("LOWER(%s) LIKE ?", field), strings.ToLower(value)+"%")
|
|
266
|
+
case "$endsL":
|
|
267
|
+
return where(fmt.Sprintf("LOWER(%s) LIKE ?", field), "%"+strings.ToLower(value))
|
|
268
|
+
case "$contL":
|
|
269
|
+
return where(fmt.Sprintf("LOWER(%s) LIKE ?", field), "%"+strings.ToLower(value)+"%")
|
|
270
|
+
case "$exclL":
|
|
271
|
+
return where(fmt.Sprintf("LOWER(%s) NOT LIKE ?", field), "%"+strings.ToLower(value)+"%")
|
|
272
|
+
case "$inL":
|
|
273
|
+
vals := splitValue(value)
|
|
274
|
+
lower := make([]string, len(vals))
|
|
275
|
+
for i, v := range vals {
|
|
276
|
+
lower[i] = strings.ToLower(v)
|
|
277
|
+
}
|
|
278
|
+
return where(fmt.Sprintf("LOWER(%s) IN ?", field), lower)
|
|
279
|
+
case "$notinL":
|
|
280
|
+
vals := splitValue(value)
|
|
281
|
+
lower := make([]string, len(vals))
|
|
282
|
+
for i, v := range vals {
|
|
283
|
+
lower[i] = strings.ToLower(v)
|
|
284
|
+
}
|
|
285
|
+
return where(fmt.Sprintf("LOWER(%s) NOT IN ?", field), lower)
|
|
286
|
+
default:
|
|
287
|
+
return query
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
func splitValue(value string) []string {
|
|
292
|
+
parts := strings.Split(value, ",")
|
|
293
|
+
result := make([]string, 0, len(parts))
|
|
294
|
+
for _, p := range parts {
|
|
295
|
+
p = strings.TrimSpace(p)
|
|
296
|
+
if p != "" {
|
|
297
|
+
result = append(result, p)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result
|
|
301
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<%~ includeFile('../../header.eta', it) %>
|
|
2
|
+
|
|
3
|
+
package utils
|
|
4
|
+
|
|
5
|
+
// ParsedFilter represents a single filter condition.
|
|
6
|
+
type ParsedFilter struct {
|
|
7
|
+
Field string
|
|
8
|
+
Operator string
|
|
9
|
+
Value string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ParsedSort represents a single sort directive.
|
|
13
|
+
type ParsedSort struct {
|
|
14
|
+
Field string
|
|
15
|
+
Direction string // "ASC" or "DESC"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ParsedJoin represents a relation to eager-load.
|
|
19
|
+
type ParsedJoin struct {
|
|
20
|
+
Relation string
|
|
21
|
+
Fields []string // nil means load all fields
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// QueryOptions holds the parsed query parameters for list endpoints.
|
|
25
|
+
type QueryOptions struct {
|
|
26
|
+
Filters []ParsedFilter
|
|
27
|
+
OrFilters []ParsedFilter
|
|
28
|
+
Sorts []ParsedSort
|
|
29
|
+
Fields []string
|
|
30
|
+
Joins []ParsedJoin
|
|
31
|
+
Limit int
|
|
32
|
+
Page int
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// PaginatedResult is the standard paginated response envelope.
|
|
36
|
+
type PaginatedResult struct {
|
|
37
|
+
Data interface{} `json:"data"`
|
|
38
|
+
Count int `json:"count"`
|
|
39
|
+
Total int64 `json:"total"`
|
|
40
|
+
Page int `json:"page"`
|
|
41
|
+
PageCount int `json:"pageCount"`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// DefaultQueryOptions returns options with default pagination values.
|
|
45
|
+
func DefaultQueryOptions() QueryOptions {
|
|
46
|
+
return QueryOptions{
|
|
47
|
+
Limit: 25,
|
|
48
|
+
Page: 1,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%~ includeFile('../header.eta', it) %>
|
|
2
2
|
|
|
3
3
|
from typing import List, Optional
|
|
4
|
-
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
4
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
6
|
|
|
7
7
|
from app.database import get_db
|
|
@@ -14,6 +14,7 @@ from ..schemas.<%= it.entityName.toLowerCase() %> import (
|
|
|
14
14
|
<%= it.entityName %>List,
|
|
15
15
|
)
|
|
16
16
|
from ..services.<%= it.entityName.toLowerCase() %> import <%= it.entityName %>Service
|
|
17
|
+
from ..utils.query import parse_query_params
|
|
17
18
|
|
|
18
19
|
router = APIRouter(
|
|
19
20
|
prefix="/<%= it.pluralEntityName.toLowerCase() %>",
|
|
@@ -27,39 +28,30 @@ router = APIRouter(
|
|
|
27
28
|
summary="Retrieve multiple <%= it.pluralEntityName %>",
|
|
28
29
|
)
|
|
29
30
|
async def get_many(
|
|
31
|
+
request: Request,
|
|
30
32
|
db: AsyncSession = Depends(get_db),
|
|
31
|
-
skip: int = Query(0, ge=0),
|
|
32
|
-
limit: int = Query(10, ge=1, le=100),
|
|
33
|
-
sort: Optional[str] = Query(None, description="Sort field (prefix with - for descending)"),
|
|
34
|
-
<% /* Add filter parameters for common fields */ %>
|
|
35
|
-
<% it.associations.filter(a => a.type === 'ManyToOne').forEach((assoc) => { %>
|
|
36
|
-
<%= assoc.camelCasedId %>: Optional[<% if (assoc.referencedEntityPrimaryKeyType === 'uuid') { %>str<% } else { %>int<% } %>] = Query(None),
|
|
37
|
-
<% }) %>
|
|
38
33
|
) -> <%= it.entityName %>List:
|
|
39
34
|
"""
|
|
40
35
|
Retrieve a paginated list of <%= it.pluralEntityName %>.
|
|
36
|
+
|
|
37
|
+
Supports query parameters:
|
|
38
|
+
- filter=field||$operator||value (repeatable)
|
|
39
|
+
- or=field||$operator||value (repeatable)
|
|
40
|
+
- sort=field,ASC (repeatable)
|
|
41
|
+
- fields=field1,field2
|
|
42
|
+
- join=relation (repeatable)
|
|
43
|
+
- limit=25&page=1
|
|
41
44
|
"""
|
|
42
45
|
service = <%= it.entityName %>Service(db)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<% it.associations.filter(a => a.type === 'ManyToOne').forEach((assoc) => { %>
|
|
46
|
-
if <%= assoc.camelCasedId %> is not None:
|
|
47
|
-
filters["<%= assoc.camelCasedId %>"] = <%= assoc.camelCasedId %>
|
|
48
|
-
<% }) %>
|
|
49
|
-
|
|
50
|
-
items, total = await service.get_many(
|
|
51
|
-
skip=skip,
|
|
52
|
-
limit=limit,
|
|
53
|
-
sort=sort,
|
|
54
|
-
filters=filters,
|
|
55
|
-
)
|
|
46
|
+
options = parse_query_params(request.query_params)
|
|
47
|
+
result = await service.get_many(options)
|
|
56
48
|
|
|
57
49
|
return <%= it.entityName %>List(
|
|
58
|
-
data=
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
data=result.data,
|
|
51
|
+
count=result.count,
|
|
52
|
+
total=result.total,
|
|
53
|
+
page=result.page,
|
|
54
|
+
pageCount=result.page_count,
|
|
63
55
|
)
|
|
64
56
|
|
|
65
57
|
|
|
@@ -70,31 +62,19 @@ async def get_many(
|
|
|
70
62
|
)
|
|
71
63
|
async def get_one(
|
|
72
64
|
id: <% if (it.primaryKeyType === 'uuid') { %>str<% } else { %>int<% } %>,
|
|
65
|
+
request: Request,
|
|
73
66
|
db: AsyncSession = Depends(get_db),
|
|
74
|
-
<% /* Add join parameters */ %>
|
|
75
|
-
<% it.associations.forEach((assoc) => { %>
|
|
76
|
-
<% if (assoc.type === 'OneToMany' || assoc.type === 'ManyToMany') { %>
|
|
77
|
-
join_<%= assoc.pluralizedRelationshipName %>: bool = Query(False, alias="join[<%= assoc.pluralizedRelationshipName %>]"),
|
|
78
|
-
<% } else { %>
|
|
79
|
-
join_<%= assoc.relationshipName %>: bool = Query(False, alias="join[<%= assoc.relationshipName %>]"),
|
|
80
|
-
<% } %>
|
|
81
|
-
<% }) %>
|
|
82
67
|
) -> <%= it.entityName %>Schema:
|
|
83
68
|
"""
|
|
84
69
|
Retrieve a single <%= it.entityName %> by ID.
|
|
70
|
+
|
|
71
|
+
Supports query parameters:
|
|
72
|
+
- join=relation (repeatable)
|
|
85
73
|
"""
|
|
86
74
|
service = <%= it.entityName %>Service(db)
|
|
87
75
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<% if (assoc.type === 'OneToMany' || assoc.type === 'ManyToMany') { %>
|
|
91
|
-
if join_<%= assoc.pluralizedRelationshipName %>:
|
|
92
|
-
joins.append("<%= assoc.pluralizedRelationshipName %>")
|
|
93
|
-
<% } else { %>
|
|
94
|
-
if join_<%= assoc.relationshipName %>:
|
|
95
|
-
joins.append("<%= assoc.relationshipName %>")
|
|
96
|
-
<% } %>
|
|
97
|
-
<% }) %>
|
|
76
|
+
options = parse_query_params(request.query_params)
|
|
77
|
+
joins = [j.relation for j in options.joins] if options.joins else None
|
|
98
78
|
|
|
99
79
|
item = await service.get_one(id, joins=joins)
|
|
100
80
|
if not item:
|
|
@@ -81,7 +81,7 @@ class <%= it.entityName %>(BaseModel):
|
|
|
81
81
|
class <%= it.entityName %>List(BaseModel):
|
|
82
82
|
"""Paginated list response for <%= it.entityName %>"""
|
|
83
83
|
data: List[<%= it.entityName %>]
|
|
84
|
+
count: int
|
|
84
85
|
total: int
|
|
85
86
|
page: int
|
|
86
|
-
|
|
87
|
-
totalPages: int
|
|
87
|
+
pageCount: int
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<%~ includeFile('../header.eta', it) %>
|
|
2
2
|
|
|
3
|
-
from typing import List, Optional,
|
|
4
|
-
|
|
3
|
+
from typing import List, Optional, Any
|
|
4
|
+
|
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
-
from sqlalchemy.orm import selectinload
|
|
7
6
|
|
|
8
7
|
from ..models.<%= it.entityName.toLowerCase() %> import <%= it.entityName %>
|
|
9
8
|
from ..schemas.<%= it.entityName.toLowerCase() %> import <%= it.entityName %>Create, <%= it.entityName %>Update
|
|
9
|
+
from ..utils.query import QueryBuilder, QueryOptions, PaginatedResult
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class <%= it.entityName %>Service:
|
|
@@ -16,59 +16,13 @@ class <%= it.entityName %>Service:
|
|
|
16
16
|
|
|
17
17
|
def __init__(self, db: AsyncSession):
|
|
18
18
|
self.db = db
|
|
19
|
+
self._qb = QueryBuilder(<%= it.entityName %>, db)
|
|
19
20
|
|
|
20
|
-
async def get_many(
|
|
21
|
-
self,
|
|
22
|
-
skip: int = 0,
|
|
23
|
-
limit: int = 10,
|
|
24
|
-
sort: Optional[str] = None,
|
|
25
|
-
filters: Optional[Dict[str, Any]] = None,
|
|
26
|
-
joins: Optional[List[str]] = None,
|
|
27
|
-
) -> Tuple[List[<%= it.entityName %>], int]:
|
|
21
|
+
async def get_many(self, options: QueryOptions) -> PaginatedResult:
|
|
28
22
|
"""
|
|
29
|
-
Retrieve a paginated list of <%= it.pluralEntityName
|
|
23
|
+
Retrieve a paginated list of <%= it.pluralEntityName %> with filtering, sorting, and joins.
|
|
30
24
|
"""
|
|
31
|
-
|
|
32
|
-
query = select(<%= it.entityName %>)
|
|
33
|
-
|
|
34
|
-
# Apply filters
|
|
35
|
-
if filters:
|
|
36
|
-
for field, value in filters.items():
|
|
37
|
-
if hasattr(<%= it.entityName %>, field) and value is not None:
|
|
38
|
-
query = query.where(getattr(<%= it.entityName %>, field) == value)
|
|
39
|
-
|
|
40
|
-
# Apply sorting
|
|
41
|
-
if sort:
|
|
42
|
-
if sort.startswith("-"):
|
|
43
|
-
query = query.order_by(desc(getattr(<%= it.entityName %>, sort[1:])))
|
|
44
|
-
else:
|
|
45
|
-
query = query.order_by(asc(getattr(<%= it.entityName %>, sort)))
|
|
46
|
-
else:
|
|
47
|
-
query = query.order_by(<%= it.entityName %>.id)
|
|
48
|
-
|
|
49
|
-
# Apply joins
|
|
50
|
-
if joins:
|
|
51
|
-
for join in joins:
|
|
52
|
-
if hasattr(<%= it.entityName %>, join):
|
|
53
|
-
query = query.options(selectinload(getattr(<%= it.entityName %>, join)))
|
|
54
|
-
|
|
55
|
-
# Get total count
|
|
56
|
-
count_query = select(func.count()).select_from(<%= it.entityName %>)
|
|
57
|
-
if filters:
|
|
58
|
-
for field, value in filters.items():
|
|
59
|
-
if hasattr(<%= it.entityName %>, field) and value is not None:
|
|
60
|
-
count_query = count_query.where(getattr(<%= it.entityName %>, field) == value)
|
|
61
|
-
total_result = await self.db.execute(count_query)
|
|
62
|
-
total = total_result.scalar() or 0
|
|
63
|
-
|
|
64
|
-
# Apply pagination
|
|
65
|
-
query = query.offset(skip).limit(limit)
|
|
66
|
-
|
|
67
|
-
# Execute query
|
|
68
|
-
result = await self.db.execute(query)
|
|
69
|
-
items = list(result.scalars().all())
|
|
70
|
-
|
|
71
|
-
return items, total
|
|
25
|
+
return await self._qb.get_many(options)
|
|
72
26
|
|
|
73
27
|
async def get_one(
|
|
74
28
|
self,
|
|
@@ -78,16 +32,7 @@ class <%= it.entityName %>Service:
|
|
|
78
32
|
"""
|
|
79
33
|
Retrieve a single <%= it.entityName %> by ID.
|
|
80
34
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Apply joins
|
|
84
|
-
if joins:
|
|
85
|
-
for join in joins:
|
|
86
|
-
if hasattr(<%= it.entityName %>, join):
|
|
87
|
-
query = query.options(selectinload(getattr(<%= it.entityName %>, join)))
|
|
88
|
-
|
|
89
|
-
result = await self.db.execute(query)
|
|
90
|
-
return result.scalar_one_or_none()
|
|
35
|
+
return await self._qb.get_one(id, joins=joins)
|
|
91
36
|
|
|
92
37
|
async def create_one(self, data: <%= it.entityName %>Create) -> <%= it.entityName %>:
|
|
93
38
|
"""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%~ includeFile('../header.eta', it) %>
|
|
2
|
+
|
|
3
|
+
from .query import (
|
|
4
|
+
parse_query_params,
|
|
5
|
+
QueryBuilder,
|
|
6
|
+
QueryOptions,
|
|
7
|
+
PaginatedResult,
|
|
8
|
+
ParsedFilter,
|
|
9
|
+
ParsedSort,
|
|
10
|
+
ParsedJoin,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"parse_query_params",
|
|
15
|
+
"QueryBuilder",
|
|
16
|
+
"QueryOptions",
|
|
17
|
+
"PaginatedResult",
|
|
18
|
+
"ParsedFilter",
|
|
19
|
+
"ParsedSort",
|
|
20
|
+
"ParsedJoin",
|
|
21
|
+
]
|