@apso/cli 0.11.1 → 0.13.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}`);
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
+ const path = tslib_1.__importStar(require("path"));
4
5
  const core_1 = require("@oclif/core");
5
6
  const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
6
7
  const apsorc_parser_1 = require("../lib/apsorc-parser");
7
- const snapshot_1 = require("../lib/migrate/snapshot");
8
8
  const sandbox_1 = require("../lib/migrate/sandbox");
9
+ const lib_1 = require("../lib");
10
+ const file_system_1 = require("../lib/utils/file-system");
9
11
  class Migrate extends base_command_1.default {
10
12
  async run() {
11
13
  const { flags } = await this.parse(Migrate);
@@ -24,37 +26,6 @@ class Migrate extends base_command_1.default {
24
26
  const msg = error instanceof Error ? error.message : String(error);
25
27
  this.error(`Failed to parse .apsorc: ${msg}`);
26
28
  }
27
- // Quick check against snapshot
28
- const snapshot = (0, snapshot_1.readSnapshot)();
29
- const currentComparable = {
30
- entities: parsed.entities.map((e) => {
31
- var _a;
32
- return ({
33
- name: e.name,
34
- fields: (_a = e.fields) === null || _a === void 0 ? void 0 : _a.map((f) => {
35
- var _a;
36
- return ({
37
- name: f.name,
38
- type: f.type,
39
- nullable: f.nullable,
40
- unique: f.unique,
41
- default: (_a = f.default) !== null && _a !== void 0 ? _a : undefined,
42
- index: f.index,
43
- });
44
- }),
45
- primaryKeyType: e.primaryKeyType,
46
- created_at: e.created_at,
47
- updated_at: e.updated_at,
48
- scopeBy: e.scopeBy,
49
- });
50
- }),
51
- };
52
- if (!(0, snapshot_1.hasChanges)(currentComparable, snapshot)) {
53
- if (!flags.sql) {
54
- this.log("Schema unchanged. No migration needed.");
55
- }
56
- return;
57
- }
58
29
  if (!flags.sql) {
59
30
  this.log("Schema changes detected. Running migration sandbox...");
60
31
  }
@@ -85,6 +56,29 @@ class Migrate extends base_command_1.default {
85
56
  this.log("");
86
57
  this.log(`${result.upSql.length} statement(s) generated and tested.`);
87
58
  this.log("Migration validated against local PGlite.");
59
+ // Generate language-native migration files for Python and Go
60
+ const language = parsed.language || "typescript";
61
+ if (language !== "typescript") {
62
+ const generatorConfig = {
63
+ language,
64
+ rootFolder: parsed.rootFolder || "src",
65
+ apiType: parsed.apiType || "rest",
66
+ entities: parsed.entities,
67
+ relationshipMap: parsed.relationshipMap,
68
+ };
69
+ const generator = (0, lib_1.createGenerator)(generatorConfig);
70
+ const migrationFiles = await generator.generateMigration({
71
+ entities: parsed.entities,
72
+ relationshipMap: parsed.relationshipMap,
73
+ upSql: result.upSql,
74
+ downSql: result.downSql,
75
+ });
76
+ for (const file of migrationFiles) {
77
+ const fullPath = path.join(process.cwd(), file.path);
78
+ await (0, file_system_1.createFile)(fullPath, file.content);
79
+ this.log(`Generated migration file: ${file.path}`);
80
+ }
81
+ }
88
82
  if (flags.apply) {
89
83
  (0, sandbox_1.applyMigration)({
90
84
  entities: parsed.entities,
@@ -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
  */
@@ -30,8 +30,9 @@ export declare class GoGenerator extends BaseGenerator {
30
30
  generateService(options: ServiceGenerationOptions): Promise<GeneratedFile[]>;
31
31
  generateDto(options: DtoGenerationOptions): Promise<GeneratedFile[]>;
32
32
  generateModule(_options: EntityGenerationOptions): Promise<GeneratedFile[]>;
33
- generateMigration(_options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
33
+ generateMigration(options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
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);
@@ -272,10 +278,31 @@ class GoGenerator extends base_1.BaseGenerator {
272
278
  // Route registration is handled in the index module
273
279
  return [];
274
280
  }
275
- async generateMigration(_options) {
276
- // Goose handles migrations differently
277
- // This is a placeholder for future migration template generation
278
- return [];
281
+ async generateMigration(options) {
282
+ const { upSql, downSql, migrationName } = options;
283
+ if (!upSql || upSql.length === 0) {
284
+ return [];
285
+ }
286
+ const timestamp = new Date()
287
+ .toISOString()
288
+ .replace(/[-:T]/g, "")
289
+ .slice(0, 14);
290
+ const name = migrationName || "auto";
291
+ const filename = `${timestamp}_${name}.sql`;
292
+ const upStatements = upSql.map((sql) => `${sql};`).join("\n");
293
+ const downStatements = (downSql || []).map((sql) => `${sql};`).join("\n");
294
+ const content = `-- +goose Up
295
+ ${upStatements}
296
+
297
+ -- +goose Down
298
+ ${downStatements}
299
+ `;
300
+ return [
301
+ {
302
+ path: `migrations/${filename}`,
303
+ content,
304
+ },
305
+ ];
279
306
  }
280
307
  async generateEnums(entities, _apiType) {
281
308
  if (entities.length === 0) {
@@ -318,11 +345,32 @@ class GoGenerator extends base_1.BaseGenerator {
318
345
  const content = await this.renderTemplate("./index-module", {
319
346
  entities: entitiesData,
320
347
  });
348
+ // Generate model registry for AutoMigrate support
349
+ const modelImports = entities
350
+ .map((e) => `\t// ${e.name} model`)
351
+ .join("\n");
352
+ const modelRefs = entities
353
+ .map((e) => `\t\t&${(0, casing_1.pascalCase)(e.name)}{},`)
354
+ .join("\n");
355
+ const registryContent = `package models
356
+
357
+ // All returns all registered GORM models for AutoMigrate.
358
+ // This file is auto-generated by the Apso CLI.
359
+ func All() []interface{} {
360
+ \treturn []interface{}{
361
+ ${modelRefs}
362
+ \t}
363
+ }
364
+ `;
321
365
  return [
322
366
  {
323
367
  path: "routes/routes.go",
324
368
  content,
325
369
  },
370
+ {
371
+ path: "models/registry.go",
372
+ content: registryContent,
373
+ },
326
374
  ];
327
375
  }
328
376
  async generateGuards(_entities, auth) {
@@ -336,5 +384,13 @@ class GoGenerator extends base_1.BaseGenerator {
336
384
  },
337
385
  ];
338
386
  }
387
+ async generateQueryUtils(_entities, _apiType) {
388
+ const typesContent = await this.renderTemplate("./utils/types", {});
389
+ const queryContent = await this.renderTemplate("./utils/query", {});
390
+ return [
391
+ { path: "utils/types.go", content: typesContent },
392
+ { path: "utils/query.go", content: queryContent },
393
+ ];
394
+ }
339
395
  }
340
396
  exports.GoGenerator = GoGenerator;
@@ -14,8 +14,9 @@ export declare class PythonGenerator extends BaseGenerator {
14
14
  generateService(options: ServiceGenerationOptions): Promise<GeneratedFile[]>;
15
15
  generateDto(options: DtoGenerationOptions): Promise<GeneratedFile[]>;
16
16
  generateModule(_options: EntityGenerationOptions): Promise<GeneratedFile[]>;
17
- generateMigration(_options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
17
+ generateMigration(options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
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
  }
@@ -206,10 +206,46 @@ class PythonGenerator extends base_1.BaseGenerator {
206
206
  // The router registration is handled in the index module
207
207
  return [];
208
208
  }
209
- async generateMigration(_options) {
210
- // Alembic handles migrations differently
211
- // This is a placeholder for future migration template generation
212
- return [];
209
+ async generateMigration(options) {
210
+ const { upSql, downSql, migrationName } = options;
211
+ if (!upSql || upSql.length === 0) {
212
+ return [];
213
+ }
214
+ const timestamp = new Date()
215
+ .toISOString()
216
+ .replace(/[-:T]/g, "")
217
+ .slice(0, 14);
218
+ const revision = timestamp;
219
+ const name = migrationName || "auto";
220
+ const filename = `${timestamp}_${name}.py`;
221
+ const upStatements = upSql
222
+ .map((sql) => ` op.execute("""${sql}""")`)
223
+ .join("\n");
224
+ const downStatements = (downSql || [])
225
+ .map((sql) => ` op.execute("""${sql}""")`)
226
+ .join("\n");
227
+ const content = `"""${name}"""
228
+ from alembic import op
229
+
230
+ revision = '${revision}'
231
+ down_revision = None
232
+ branch_labels = None
233
+ depends_on = None
234
+
235
+
236
+ def upgrade() -> None:
237
+ ${upStatements}
238
+
239
+
240
+ def downgrade() -> None:
241
+ ${downStatements || " pass"}
242
+ `;
243
+ return [
244
+ {
245
+ path: `alembic/versions/${filename}`,
246
+ content,
247
+ },
248
+ ];
213
249
  }
214
250
  async generateEnums(entities, _apiType) {
215
251
  if (entities.length === 0) {
@@ -250,5 +286,13 @@ class PythonGenerator extends base_1.BaseGenerator {
250
286
  // This is a placeholder for future implementation
251
287
  return [];
252
288
  }
289
+ async generateQueryUtils(_entities, _apiType) {
290
+ const queryContent = await this.renderTemplate("./utils/query", {});
291
+ const initContent = await this.renderTemplate("./utils/init", {});
292
+ return [
293
+ { path: "utils/query.py", content: queryContent },
294
+ { path: "utils/__init__.py", content: initContent },
295
+ ];
296
+ }
253
297
  }
254
298
  exports.PythonGenerator = PythonGenerator;
@@ -1,11 +1,12 @@
1
1
  <% const {field} = it; %>
2
2
 
3
+ <% if (field.primary ) {%>
4
+ @PrimaryGeneratedColumn('uuid')
5
+ <% } else { %>
3
6
  @Column({ "type": "uuid"<% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
4
7
  <% if (field.index ) {%>
5
8
  @Index()
6
9
  <% } %>
7
- <% if (field.primary ) {%>
8
- @PrimaryColumn()
9
10
  <% } %>
10
11
 
11
- <%= field.name %>: string;
12
+ <%= field.name %>: string;
@@ -58,8 +58,7 @@ export class <%= it.name %> {
58
58
  <% if (it.createPrimaryKey) { %>
59
59
  // Fields
60
60
  <% if (it.primaryKeyType === 'uuid') { %>
61
- @Column({ type: 'uuid' })
62
- @PrimaryColumn()
61
+ @PrimaryGeneratedColumn('uuid')
63
62
  id: string;
64
63
  <% } else if (it.primaryKeyType === 'text') { %>
65
64
  @Column({ type: 'text' })
@@ -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