@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.
@@ -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 []<%= it.entityName %>Response `json:"data"`
74
- Total int64 `json:"total"`
75
- Page int `json:"page"`
76
- PageSize int `json:"pageSize"`
77
- TotalPages int `json:"totalPages"`
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 page query int false "Page number" default(1)
47
- // @Param pageSize query int false "Page size" default(10)
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
- page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
53
- pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
58
+ opts := utils.ParseQueryParams(c.Request.URL.Query())
54
59
 
55
- if page < 1 {
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
@@ -3,8 +3,8 @@
3
3
  package routes
4
4
 
5
5
  import (
6
- "app/handlers"
7
- "app/services"
6
+ "app/autogen/handlers"
7
+ "app/autogen/services"
8
8
  "github.com/gin-gonic/gin"
9
9
  "gorm.io/gorm"
10
10
  )
@@ -5,9 +5,8 @@ package models
5
5
  import (
6
6
  "time"
7
7
  <% if (it.importEnums) { %>
8
- "app/models/enums"
8
+ "app/autogen/models/enums"
9
9
  <% } %>
10
- "gorm.io/gorm"
11
10
  )
12
11
 
13
12
  // <%= it.name %> represents the <%= it.snakeCasedName %> table
@@ -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, page, pageSize int) (*dto.<%= it.entityName %>PaginatedResponse, error) {
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
- offset := (page - 1) * pageSize
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
- totalPages := int(total) / pageSize
42
- if int(total)%pageSize > 0 {
43
- totalPages++
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: response,
53
- Total: total,
54
- Page: page,
55
- PageSize: pageSize,
56
- TotalPages: totalPages,
62
+ Data: response,
63
+ Count: len(response),
64
+ Total: total,
65
+ Page: opts.Page,
66
+ PageCount: pageCount,
57
67
  }, nil
58
68
  }
59
69