@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.
- package/dist/commands/generate.js +7 -0
- package/dist/commands/migrate.js +26 -32
- package/dist/lib/generators/base.d.ts +5 -0
- package/dist/lib/generators/base.js +7 -0
- package/dist/lib/generators/go.d.ts +2 -1
- package/dist/lib/generators/go.js +60 -4
- package/dist/lib/generators/python.d.ts +2 -1
- package/dist/lib/generators/python.js +48 -4
- package/dist/lib/templates/entities/entity-col-uuid.eta +4 -3
- package/dist/lib/templates/entities/entity.eta +1 -2
- 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 +12 -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}`);
|
package/dist/commands/migrate.js
CHANGED
|
@@ -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(
|
|
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(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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(
|
|
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(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
|