@apso/cli 0.11.1 → 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/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 +1 -1
|
@@ -75,6 +75,13 @@ class Generate extends base_command_1.default {
|
|
|
75
75
|
// eslint-disable-next-line no-await-in-loop
|
|
76
76
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
77
77
|
}
|
|
78
|
+
// Generate shared query utilities
|
|
79
|
+
const queryUtilFiles = await generator.generateQueryUtils(entities, lowerCaseApiType);
|
|
80
|
+
for (const file of queryUtilFiles) {
|
|
81
|
+
const fullPath = path.join(autogenPath, file.path);
|
|
82
|
+
// eslint-disable-next-line no-await-in-loop
|
|
83
|
+
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
84
|
+
}
|
|
78
85
|
// Generate per-entity files
|
|
79
86
|
for (const entity of entities) {
|
|
80
87
|
console.log(`[apso] Building... ${entity.name}`);
|
|
@@ -57,6 +57,11 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
|
|
|
57
57
|
* Generate guard files (auth, scope, etc.)
|
|
58
58
|
*/
|
|
59
59
|
abstract generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Generate shared query utility files (filter, sort, pagination).
|
|
62
|
+
* Default no-op. Override in language generators that need query utils.
|
|
63
|
+
*/
|
|
64
|
+
generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
60
65
|
/**
|
|
61
66
|
* Render a template file with the given data
|
|
62
67
|
*/
|
|
@@ -20,6 +20,13 @@ class BaseGenerator {
|
|
|
20
20
|
cache: false,
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate shared query utility files (filter, sort, pagination).
|
|
25
|
+
* Default no-op. Override in language generators that need query utils.
|
|
26
|
+
*/
|
|
27
|
+
async generateQueryUtils(_entities, _apiType) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
23
30
|
/**
|
|
24
31
|
* Render a template file with the given data
|
|
25
32
|
*/
|
|
@@ -34,4 +34,5 @@ export declare class GoGenerator extends BaseGenerator {
|
|
|
34
34
|
generateEnums(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
35
35
|
generateIndexModule(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
36
36
|
generateGuards(_entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
37
|
+
generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
37
38
|
}
|
|
@@ -20,6 +20,7 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
20
20
|
Eta.configure({
|
|
21
21
|
views: this.templateBasePath,
|
|
22
22
|
cache: false,
|
|
23
|
+
autoTrim: false,
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
26
|
getLanguage() {
|
|
@@ -142,6 +143,9 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
142
143
|
primaryKeyGoType: this.getPrimaryKeyGoType(primaryKeyType),
|
|
143
144
|
camelReferenceName: (0, casing_1.camelCase)(rel.referenceName || rel.name),
|
|
144
145
|
snakeReferenceName: (0, casing_1.snakeCase)(rel.referenceName || rel.name),
|
|
146
|
+
pluralName: (0, casing_1.pascalCase)((0, pluralize_1.default)(rel.name)),
|
|
147
|
+
camelPluralName: (0, casing_1.camelCase)((0, pluralize_1.default)(rel.name)),
|
|
148
|
+
inverseName: (0, casing_1.pascalCase)(rel.inversePropertyName || name),
|
|
145
149
|
}));
|
|
146
150
|
const entitiesToImport = (0, relationships_1.getRelationshipsForImport)(name, relationshipsForTemplate);
|
|
147
151
|
const data = {
|
|
@@ -241,6 +245,8 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
241
245
|
...rel,
|
|
242
246
|
primaryKeyGoType: this.getPrimaryKeyGoType(referencedPrimaryKeyType),
|
|
243
247
|
camelReferenceName: (0, casing_1.camelCase)(rel.referenceName || rel.name),
|
|
248
|
+
pluralName: (0, casing_1.pascalCase)((0, pluralize_1.default)(rel.name)),
|
|
249
|
+
camelPluralName: (0, casing_1.camelCase)((0, pluralize_1.default)(rel.name)),
|
|
244
250
|
};
|
|
245
251
|
});
|
|
246
252
|
const primaryKeyColumns = columns.filter((column) => column.primary === true);
|
|
@@ -336,5 +342,13 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
336
342
|
},
|
|
337
343
|
];
|
|
338
344
|
}
|
|
345
|
+
async generateQueryUtils(_entities, _apiType) {
|
|
346
|
+
const typesContent = await this.renderTemplate("./utils/types", {});
|
|
347
|
+
const queryContent = await this.renderTemplate("./utils/query", {});
|
|
348
|
+
return [
|
|
349
|
+
{ path: "utils/types.go", content: typesContent },
|
|
350
|
+
{ path: "utils/query.go", content: queryContent },
|
|
351
|
+
];
|
|
352
|
+
}
|
|
339
353
|
}
|
|
340
354
|
exports.GoGenerator = GoGenerator;
|
|
@@ -18,4 +18,5 @@ export declare class PythonGenerator extends BaseGenerator {
|
|
|
18
18
|
generateEnums(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
19
19
|
generateIndexModule(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
20
20
|
generateGuards(_entities: Entity[], _auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
21
|
+
generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
|
|
21
22
|
}
|
|
@@ -250,5 +250,13 @@ class PythonGenerator extends base_1.BaseGenerator {
|
|
|
250
250
|
// This is a placeholder for future implementation
|
|
251
251
|
return [];
|
|
252
252
|
}
|
|
253
|
+
async generateQueryUtils(_entities, _apiType) {
|
|
254
|
+
const queryContent = await this.renderTemplate("./utils/query", {});
|
|
255
|
+
const initContent = await this.renderTemplate("./utils/init", {});
|
|
256
|
+
return [
|
|
257
|
+
{ path: "utils/query.py", content: queryContent },
|
|
258
|
+
{ path: "utils/__init__.py", content: initContent },
|
|
259
|
+
];
|
|
260
|
+
}
|
|
253
261
|
}
|
|
254
262
|
exports.PythonGenerator = PythonGenerator;
|
|
@@ -5,74 +5,74 @@ package dto
|
|
|
5
5
|
import (
|
|
6
6
|
"time"
|
|
7
7
|
<% if (it.importEnums) { %>
|
|
8
|
-
"app/models/enums"
|
|
8
|
+
"app/autogen/models/enums"
|
|
9
9
|
<% } %>
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
// <%= it.entityName %>Response represents the response DTO for <%= it.entityName %>
|
|
13
13
|
type <%= it.entityName %>Response struct {
|
|
14
|
-
<% if (it.addDefaultPKProperty) {
|
|
15
|
-
<% if (it.primaryKeyType === 'uuid') {
|
|
14
|
+
<% if (it.addDefaultPKProperty) { -%>
|
|
15
|
+
<% if (it.primaryKeyType === 'uuid') { -%>
|
|
16
16
|
ID string `json:"id"`
|
|
17
|
-
<% } else {
|
|
17
|
+
<% } else { -%>
|
|
18
18
|
ID uint `json:"id"`
|
|
19
|
-
<% }
|
|
20
|
-
<% }
|
|
21
|
-
<% it.columns.forEach((column) => {
|
|
19
|
+
<% } -%>
|
|
20
|
+
<% } -%>
|
|
21
|
+
<% it.columns.forEach((column) => { -%>
|
|
22
22
|
<%~ includeFile('./dto-col-' + column.type + '.eta', { column, mode: 'response' }) %>
|
|
23
|
-
<% })
|
|
24
|
-
<% if (it.createdAt) {
|
|
23
|
+
<% }); -%>
|
|
24
|
+
<% if (it.createdAt) { -%>
|
|
25
25
|
CreatedAt time.Time `json:"createdAt"`
|
|
26
|
-
<% }
|
|
27
|
-
<% if (it.updatedAt) {
|
|
26
|
+
<% } -%>
|
|
27
|
+
<% if (it.updatedAt) { -%>
|
|
28
28
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
29
|
-
<% }
|
|
30
|
-
<% it.associations.forEach((assoc) => {
|
|
31
|
-
<% if (assoc.type === 'OneToMany') {
|
|
29
|
+
<% } -%>
|
|
30
|
+
<% it.associations.forEach((assoc) => { -%>
|
|
31
|
+
<% if (assoc.type === 'OneToMany') { -%>
|
|
32
32
|
<%= assoc.pluralName %> []<%= assoc.name %>Response `json:"<%= assoc.camelPluralName %>,omitempty"`
|
|
33
|
-
<% } else if (assoc.type === 'ManyToOne') {
|
|
33
|
+
<% } else if (assoc.type === 'ManyToOne') { -%>
|
|
34
34
|
<%= assoc.referenceName || assoc.name %>ID <%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id"`
|
|
35
35
|
<%= assoc.referenceName || assoc.name %> *<%= assoc.name %>Response `json:"<%= assoc.camelReferenceName || assoc.camelName %>,omitempty"`
|
|
36
|
-
<% } else if (assoc.type === 'OneToOne') {
|
|
37
|
-
<% if (assoc.isOwner) {
|
|
36
|
+
<% } else if (assoc.type === 'OneToOne') { -%>
|
|
37
|
+
<% if (assoc.isOwner) { -%>
|
|
38
38
|
<%= assoc.referenceName || assoc.name %>ID <%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id"`
|
|
39
|
-
<% }
|
|
39
|
+
<% } -%>
|
|
40
40
|
<%= assoc.referenceName || assoc.name %> *<%= assoc.name %>Response `json:"<%= assoc.camelReferenceName || assoc.camelName %>,omitempty"`
|
|
41
|
-
<% } else if (assoc.type === 'ManyToMany') {
|
|
41
|
+
<% } else if (assoc.type === 'ManyToMany') { -%>
|
|
42
42
|
<%= assoc.pluralName %> []<%= assoc.name %>Response `json:"<%= assoc.camelPluralName %>,omitempty"`
|
|
43
|
-
<% }
|
|
44
|
-
<% })
|
|
43
|
+
<% } -%>
|
|
44
|
+
<% }) -%>
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// <%= it.entityName %>Create represents the create DTO for <%= it.entityName %>
|
|
48
48
|
type <%= it.entityName %>Create struct {
|
|
49
|
-
<% it.columns.forEach((column) => {
|
|
49
|
+
<% it.columns.forEach((column) => { -%>
|
|
50
50
|
<%~ includeFile('./dto-col-' + column.type + '.eta', { column, mode: 'create' }) %>
|
|
51
|
-
<% })
|
|
52
|
-
<% it.associations.forEach((assoc) => {
|
|
53
|
-
<% if (assoc.type === 'ManyToOne' || (assoc.type === 'OneToOne' && assoc.isOwner)) {
|
|
51
|
+
<% }); -%>
|
|
52
|
+
<% it.associations.forEach((assoc) => { -%>
|
|
53
|
+
<% if (assoc.type === 'ManyToOne' || (assoc.type === 'OneToOne' && assoc.isOwner)) { -%>
|
|
54
54
|
<%= assoc.referenceName || assoc.name %>ID <%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id" validate:"required"`
|
|
55
|
-
<% }
|
|
56
|
-
<% })
|
|
55
|
+
<% } -%>
|
|
56
|
+
<% }) -%>
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// <%= it.entityName %>Update represents the update DTO for <%= it.entityName %>
|
|
60
60
|
type <%= it.entityName %>Update struct {
|
|
61
|
-
<% it.columns.forEach((column) => {
|
|
61
|
+
<% it.columns.forEach((column) => { -%>
|
|
62
62
|
<%~ includeFile('./dto-col-' + column.type + '.eta', { column, mode: 'update' }) %>
|
|
63
|
-
<% })
|
|
64
|
-
<% it.associations.forEach((assoc) => {
|
|
65
|
-
<% if (assoc.type === 'ManyToOne' || (assoc.type === 'OneToOne' && assoc.isOwner)) {
|
|
63
|
+
<% }); -%>
|
|
64
|
+
<% it.associations.forEach((assoc) => { -%>
|
|
65
|
+
<% if (assoc.type === 'ManyToOne' || (assoc.type === 'OneToOne' && assoc.isOwner)) { -%>
|
|
66
66
|
<%= assoc.referenceName || assoc.name %>ID *<%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id,omitempty"`
|
|
67
|
-
<% }
|
|
68
|
-
<% })
|
|
67
|
+
<% } -%>
|
|
68
|
+
<% }) -%>
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// PaginatedResponse wraps paginated results for <%= it.entityName %>
|
|
71
|
+
// <%= it.entityName %>PaginatedResponse wraps paginated results for <%= it.entityName %>
|
|
72
72
|
type <%= it.entityName %>PaginatedResponse struct {
|
|
73
|
-
Data
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
Data []<%= it.entityName %>Response `json:"data"`
|
|
74
|
+
Count int `json:"count"`
|
|
75
|
+
Total int64 `json:"total"`
|
|
76
|
+
Page int `json:"page"`
|
|
77
|
+
PageCount int `json:"pageCount"`
|
|
78
78
|
}
|
|
@@ -6,8 +6,9 @@ import (
|
|
|
6
6
|
"net/http"
|
|
7
7
|
"strconv"
|
|
8
8
|
|
|
9
|
-
"app/dto"
|
|
10
|
-
"app/services"
|
|
9
|
+
"app/autogen/dto"
|
|
10
|
+
"app/autogen/services"
|
|
11
|
+
"app/autogen/utils"
|
|
11
12
|
"github.com/gin-gonic/gin"
|
|
12
13
|
"github.com/go-playground/validator/v10"
|
|
13
14
|
)
|
|
@@ -39,27 +40,24 @@ func (h *<%= it.entityName %>Handler) RegisterRoutes(r *gin.RouterGroup) {
|
|
|
39
40
|
|
|
40
41
|
// FindAll godoc
|
|
41
42
|
// @Summary List all <%= it.pluralName %>
|
|
42
|
-
// @Description Get a paginated list of <%= it.pluralName %>
|
|
43
|
+
// @Description Get a paginated list of <%= it.pluralName %> with filtering, sorting, and joins
|
|
43
44
|
// @Tags <%= it.pluralName %>
|
|
44
45
|
// @Accept json
|
|
45
46
|
// @Produce json
|
|
46
|
-
// @Param
|
|
47
|
-
// @Param
|
|
47
|
+
// @Param filter query string false "Filter: field||$operator||value (repeatable)"
|
|
48
|
+
// @Param or query string false "OR filter: field||$operator||value (repeatable)"
|
|
49
|
+
// @Param sort query string false "Sort: field,ASC or field,DESC (repeatable)"
|
|
50
|
+
// @Param fields query string false "Field selection: field1,field2"
|
|
51
|
+
// @Param join query string false "Join: relation or relation||field1,field2 (repeatable)"
|
|
52
|
+
// @Param limit query int false "Page size (max 200)" default(25)
|
|
53
|
+
// @Param page query int false "Page number" default(1)
|
|
48
54
|
// @Success 200 {object} dto.<%= it.entityName %>PaginatedResponse
|
|
49
55
|
// @Failure 500 {object} map[string]string
|
|
50
56
|
// @Router /<%= it.kebabPluralName %> [get]
|
|
51
57
|
func (h *<%= it.entityName %>Handler) FindAll(c *gin.Context) {
|
|
52
|
-
|
|
53
|
-
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
|
58
|
+
opts := utils.ParseQueryParams(c.Request.URL.Query())
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
page = 1
|
|
57
|
-
}
|
|
58
|
-
if pageSize < 1 || pageSize > 100 {
|
|
59
|
-
pageSize = 10
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
result, err := h.service.FindAll(c.Request.Context(), page, pageSize)
|
|
60
|
+
result, err := h.service.FindAll(c.Request.Context(), opts)
|
|
63
61
|
if err != nil {
|
|
64
62
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
65
63
|
return
|
|
@@ -6,11 +6,30 @@ import (
|
|
|
6
6
|
"context"
|
|
7
7
|
"errors"
|
|
8
8
|
|
|
9
|
-
"app/dto"
|
|
10
|
-
"app/models"
|
|
9
|
+
"app/autogen/dto"
|
|
10
|
+
"app/autogen/models"
|
|
11
|
+
"app/autogen/utils"
|
|
11
12
|
"gorm.io/gorm"
|
|
12
13
|
)
|
|
13
14
|
|
|
15
|
+
// AllowedFields<%= it.entityName %> defines the column names that can be used in
|
|
16
|
+
// filter and sort query parameters. This prevents arbitrary column access.
|
|
17
|
+
var AllowedFields<%= it.entityName %> = map[string]bool{
|
|
18
|
+
"id": true,
|
|
19
|
+
<% it.columns.forEach((column) => { %>
|
|
20
|
+
"<%= column.snakeName %>": true,
|
|
21
|
+
<% }) %>
|
|
22
|
+
<% if (it.createdAt) { %>
|
|
23
|
+
"created_at": true,
|
|
24
|
+
<% } %>
|
|
25
|
+
<% if (it.updatedAt) { %>
|
|
26
|
+
"updated_at": true,
|
|
27
|
+
<% } %>
|
|
28
|
+
<% it.associations.filter(a => a.type === 'ManyToOne' || (a.type === 'OneToOne' && a.isOwner)).forEach((assoc) => { %>
|
|
29
|
+
"<%= assoc.snakeReferenceName || assoc.snakeName %>_id": true,
|
|
30
|
+
<% }) %>
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
type <%= it.entityName %>Service struct {
|
|
15
34
|
db *gorm.DB
|
|
16
35
|
}
|
|
@@ -19,28 +38,19 @@ func New<%= it.entityName %>Service(db *gorm.DB) *<%= it.entityName %>Service {
|
|
|
19
38
|
return &<%= it.entityName %>Service{db: db}
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
// FindAll retrieves a paginated list of <%= it.pluralName %>
|
|
23
|
-
func (s *<%= it.entityName %>Service) FindAll(ctx context.Context,
|
|
24
|
-
var total int64
|
|
41
|
+
// FindAll retrieves a paginated list of <%= it.pluralName %> with filtering, sorting, and joins.
|
|
42
|
+
func (s *<%= it.entityName %>Service) FindAll(ctx context.Context, opts utils.QueryOptions) (*dto.<%= it.entityName %>PaginatedResponse, error) {
|
|
25
43
|
var items []models.<%= it.entityName %>
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if err := s.db.WithContext(ctx).Model(&models.<%= it.entityName %>{}).Count(&total).Error; err != nil {
|
|
30
|
-
return nil, err
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
query := s.db.WithContext(ctx)
|
|
45
|
+
query := s.db.WithContext(ctx).Model(&models.<%= it.entityName %>{})
|
|
34
46
|
<% it.associations.filter(a => a.eager).forEach((assoc) => { %>
|
|
35
47
|
query = query.Preload("<%= assoc.referenceName || assoc.name %>")
|
|
36
48
|
<% }) %>
|
|
37
|
-
if err := query.Offset(offset).Limit(pageSize).Find(&items).Error; err != nil {
|
|
38
|
-
return nil, err
|
|
39
|
-
}
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
query, total, pageCount := utils.ApplyQuery(query, opts, AllowedFields<%= it.entityName %>)
|
|
51
|
+
|
|
52
|
+
if err := query.Find(&items).Error; err != nil {
|
|
53
|
+
return nil, err
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
response := make([]dto.<%= it.entityName %>Response, len(items))
|
|
@@ -49,11 +59,11 @@ func (s *<%= it.entityName %>Service) FindAll(ctx context.Context, page, pageSiz
|
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
return &dto.<%= it.entityName %>PaginatedResponse{
|
|
52
|
-
Data:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
Data: response,
|
|
63
|
+
Count: len(response),
|
|
64
|
+
Total: total,
|
|
65
|
+
Page: opts.Page,
|
|
66
|
+
PageCount: pageCount,
|
|
57
67
|
}, nil
|
|
58
68
|
}
|
|
59
69
|
|