@vira-ui/cli 1.1.0 → 1.1.2
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/README.md +64 -0
- package/dist/index.js +194 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -457,6 +457,18 @@ vira make migration create-users
|
|
|
457
457
|
# Создаёт: migrations/20240101120000_create-users.up.sql
|
|
458
458
|
# migrations/20240101120000_create-users.down.sql
|
|
459
459
|
|
|
460
|
+
# Выполнение миграций
|
|
461
|
+
vira db migrate # Применить все миграции
|
|
462
|
+
vira db up # Алиас для migrate
|
|
463
|
+
vira db rollback # Откатить последнюю миграцию
|
|
464
|
+
vira db down # Алиас для rollback
|
|
465
|
+
vira db status # Показать статус миграций
|
|
466
|
+
|
|
467
|
+
# С указанием URL базы данных
|
|
468
|
+
vira db migrate --db-url "postgres://user:pass@localhost/dbname?sslmode=disable"
|
|
469
|
+
# Или через переменную окружения
|
|
470
|
+
DATABASE_URL="postgres://user:pass@localhost/dbname?sslmode=disable" vira db migrate
|
|
471
|
+
|
|
460
472
|
# Генерация event handler
|
|
461
473
|
vira make event user.created
|
|
462
474
|
# Создаёт: backend/internal/events/user_created.go
|
|
@@ -594,6 +606,58 @@ func DeleteUser(w http.ResponseWriter, r *http.Request) {
|
|
|
594
606
|
|
|
595
607
|
---
|
|
596
608
|
|
|
609
|
+
### `vira db` — Команды для работы с базой данных
|
|
610
|
+
|
|
611
|
+
Управление миграциями базы данных через goose.
|
|
612
|
+
|
|
613
|
+
**Подкоманды:**
|
|
614
|
+
|
|
615
|
+
#### `vira db migrate` (или `vira db up`)
|
|
616
|
+
Применить все неприменённые миграции.
|
|
617
|
+
|
|
618
|
+
**Опции:**
|
|
619
|
+
- `-d, --dir <directory>` — Директория с миграциями (по умолчанию: `migrations`)
|
|
620
|
+
- `--db-url <url>` — URL подключения к базе данных (или используйте переменную `DATABASE_URL`)
|
|
621
|
+
- `--driver <driver>` — Драйвер БД: `postgres`, `mysql`, `sqlite3` (по умолчанию: `postgres`)
|
|
622
|
+
|
|
623
|
+
**Примеры:**
|
|
624
|
+
```bash
|
|
625
|
+
# Использование переменной окружения
|
|
626
|
+
export DATABASE_URL="postgres://user:pass@localhost/mydb?sslmode=disable"
|
|
627
|
+
vira db migrate
|
|
628
|
+
|
|
629
|
+
# Или напрямую в команде
|
|
630
|
+
vira db migrate --db-url "postgres://user:pass@localhost/mydb?sslmode=disable"
|
|
631
|
+
|
|
632
|
+
# С указанием директории миграций
|
|
633
|
+
vira db migrate --dir migrations --db-url "postgres://..."
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
#### `vira db rollback` (или `vira db down`)
|
|
637
|
+
Откатить последнюю применённую миграцию.
|
|
638
|
+
|
|
639
|
+
**Опции:** те же, что у `migrate`
|
|
640
|
+
|
|
641
|
+
**Пример:**
|
|
642
|
+
```bash
|
|
643
|
+
vira db rollback
|
|
644
|
+
vira db rollback --db-url "postgres://user:pass@localhost/mydb?sslmode=disable"
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
#### `vira db status`
|
|
648
|
+
Показать статус всех миграций (какие применены, какие нет).
|
|
649
|
+
|
|
650
|
+
**Опции:** те же, что у `migrate`
|
|
651
|
+
|
|
652
|
+
**Пример:**
|
|
653
|
+
```bash
|
|
654
|
+
vira db status
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Примечание:** Для работы команд требуется установленный `goose`. CLI автоматически попытается установить его при первом использовании, если он не найден.
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
597
661
|
### `vira sync` — Синхронизация типов
|
|
598
662
|
|
|
599
663
|
Синхронизирует TypeScript типы из Go структур.
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,7 @@ const program = new commander_1.Command();
|
|
|
91
91
|
program
|
|
92
92
|
.name("vira")
|
|
93
93
|
.description("ViraJS CLI - Create projects and generate code")
|
|
94
|
-
.version("1.1.
|
|
94
|
+
.version("1.1.2");
|
|
95
95
|
const SUPPORTED_TEMPLATES = ["frontend", "fullstack", "kanban"];
|
|
96
96
|
/**
|
|
97
97
|
* Инициализация проекта в текущей директории
|
|
@@ -283,15 +283,6 @@ make
|
|
|
283
283
|
await generateGoHandler(name, options.dir);
|
|
284
284
|
console.log(chalk_1.default.green(`✓ handler ${name} created in ${options.dir}`));
|
|
285
285
|
});
|
|
286
|
-
make
|
|
287
|
-
.command("model")
|
|
288
|
-
.description("Create Go model struct")
|
|
289
|
-
.argument("<name>", "Model name (e.g. User)")
|
|
290
|
-
.option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "models"))
|
|
291
|
-
.action(async (name, options) => {
|
|
292
|
-
await generateGoModel(name, options.dir);
|
|
293
|
-
console.log(chalk_1.default.green(`✓ model ${name} created in ${options.dir}`));
|
|
294
|
-
});
|
|
295
286
|
make
|
|
296
287
|
.command("migration")
|
|
297
288
|
.description("Create SQL migration (up/down)")
|
|
@@ -301,6 +292,39 @@ make
|
|
|
301
292
|
await generateMigration(name, options.dir);
|
|
302
293
|
console.log(chalk_1.default.green(`✓ migration ${name} created in ${options.dir}`));
|
|
303
294
|
});
|
|
295
|
+
// Команды для выполнения миграций
|
|
296
|
+
const dbCommand = program
|
|
297
|
+
.command("db")
|
|
298
|
+
.description("Database migration commands");
|
|
299
|
+
dbCommand
|
|
300
|
+
.command("migrate")
|
|
301
|
+
.alias("up")
|
|
302
|
+
.description("Run database migrations")
|
|
303
|
+
.option("-d, --dir <directory>", "Migrations directory", "migrations")
|
|
304
|
+
.option("--db-url <url>", "Database connection URL (or use DATABASE_URL env var)")
|
|
305
|
+
.option("--driver <driver>", "Database driver (postgres, mysql, sqlite3)", "postgres")
|
|
306
|
+
.action(async (options) => {
|
|
307
|
+
await runMigrations(options.dir || "migrations", options.dbUrl, options.driver || "postgres", "up");
|
|
308
|
+
});
|
|
309
|
+
dbCommand
|
|
310
|
+
.command("rollback")
|
|
311
|
+
.alias("down")
|
|
312
|
+
.description("Rollback last migration")
|
|
313
|
+
.option("-d, --dir <directory>", "Migrations directory", "migrations")
|
|
314
|
+
.option("--db-url <url>", "Database connection URL (or use DATABASE_URL env var)")
|
|
315
|
+
.option("--driver <driver>", "Database driver (postgres, mysql, sqlite3)", "postgres")
|
|
316
|
+
.action(async (options) => {
|
|
317
|
+
await runMigrations(options.dir || "migrations", options.dbUrl, options.driver || "postgres", "down");
|
|
318
|
+
});
|
|
319
|
+
dbCommand
|
|
320
|
+
.command("status")
|
|
321
|
+
.description("Show migration status")
|
|
322
|
+
.option("-d, --dir <directory>", "Migrations directory", "migrations")
|
|
323
|
+
.option("--db-url <url>", "Database connection URL (or use DATABASE_URL env var)")
|
|
324
|
+
.option("--driver <driver>", "Database driver (postgres, mysql, sqlite3)", "postgres")
|
|
325
|
+
.action(async (options) => {
|
|
326
|
+
await showMigrationStatus(options.dir || "migrations", options.dbUrl, options.driver || "postgres");
|
|
327
|
+
});
|
|
304
328
|
make
|
|
305
329
|
.command("event")
|
|
306
330
|
.description("Create Go event handler stub")
|
|
@@ -837,7 +861,29 @@ ${props.map((p) => ` ${p.name}${p.required ? "" : "?"}: ${p.type};`).join("\n")
|
|
|
837
861
|
function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
|
|
838
862
|
const propsUsage = props.map((p) => ` ${p.name}={props.${p.name}}`).join("\n");
|
|
839
863
|
if (vrpConfig) {
|
|
840
|
-
|
|
864
|
+
// Проверяем, есть ли в channel плейсхолдер {id} или подобный
|
|
865
|
+
const hasPlaceholder = vrpConfig.channel.includes('{id}') || vrpConfig.channel.includes('${id}');
|
|
866
|
+
let channelCode = '';
|
|
867
|
+
if (hasPlaceholder && hasProps) {
|
|
868
|
+
// Находим prop, который может быть id (clientId, userId, id и т.д.)
|
|
869
|
+
const idProp = props.find(p => p.name.toLowerCase().includes('id') ||
|
|
870
|
+
p.name === 'id');
|
|
871
|
+
if (idProp) {
|
|
872
|
+
// Заменяем {id} на значение из props
|
|
873
|
+
const placeholder = vrpConfig.channel.includes('${id}') ? '${id}' : '{id}';
|
|
874
|
+
const channelTemplate = vrpConfig.channel.replace(placeholder, `\${props.${idProp.name}}`);
|
|
875
|
+
channelCode = ` // Динамически формируем channel с ${idProp.name} из props
|
|
876
|
+
const channel = \`${channelTemplate}\`;
|
|
877
|
+
const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>(channel, null);`;
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
channelCode = ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);`;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
channelCode = ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);`;
|
|
885
|
+
}
|
|
886
|
+
return `${channelCode}
|
|
841
887
|
|
|
842
888
|
// Use data from VRP state
|
|
843
889
|
// Example: const value = data?.field ?? defaultValue;
|
|
@@ -850,7 +896,7 @@ function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
|
|
|
850
896
|
// }, { debounce: 500 });
|
|
851
897
|
|
|
852
898
|
return createElement('div', { className: '${name.toLowerCase()}' },
|
|
853
|
-
|
|
899
|
+
// Add your content here
|
|
854
900
|
);`;
|
|
855
901
|
}
|
|
856
902
|
if (useViraUI) {
|
|
@@ -897,7 +943,12 @@ async function generateComponent(name, dir, config) {
|
|
|
897
943
|
`;
|
|
898
944
|
// Интерфейс состояния VRP
|
|
899
945
|
if (vrpConfig) {
|
|
900
|
-
|
|
946
|
+
const typeName = vrpConfig.stateType.replace('State', '');
|
|
947
|
+
componentCode += `// TODO: Если у вас есть синхронизированные типы из backend, используйте их:
|
|
948
|
+
// import type { ${typeName} } from '../vira-types';
|
|
949
|
+
// export type ${vrpConfig.stateType} = ${typeName};
|
|
950
|
+
|
|
951
|
+
export interface ${vrpConfig.stateType} {
|
|
901
952
|
// Add your state fields here
|
|
902
953
|
id?: string;
|
|
903
954
|
}
|
|
@@ -976,10 +1027,15 @@ async function collectServiceVRPConfig(name, useVRP, interactive) {
|
|
|
976
1027
|
*/
|
|
977
1028
|
function buildVRPService(name, vrpConfig) {
|
|
978
1029
|
const lowerName = name.toLowerCase();
|
|
1030
|
+
// Пытаемся определить имя типа из синхронизированных типов (например, Client вместо ClientState)
|
|
1031
|
+
const typeName = vrpConfig.stateType.replace('State', '');
|
|
979
1032
|
return `// ${name} service using Vira Core DI container + VRP
|
|
980
|
-
import { createService, useService } from '@vira-ui/core';
|
|
1033
|
+
import { createService, useService, batch } from '@vira-ui/core';
|
|
981
1034
|
import { useViraState } from '@vira-ui/react';
|
|
982
1035
|
import { v4 as uuid } from 'uuid';
|
|
1036
|
+
// TODO: Если у вас есть синхронизированные типы из backend, используйте их:
|
|
1037
|
+
// import type { ${typeName} } from '../vira-types';
|
|
1038
|
+
// export type ${vrpConfig.stateType} = ${typeName};
|
|
983
1039
|
|
|
984
1040
|
export interface ${vrpConfig.stateType} {
|
|
985
1041
|
// Add your state fields here
|
|
@@ -1009,22 +1065,26 @@ createService('${lowerName}', () => ({
|
|
|
1009
1065
|
// Hook for ${name} operations (combines service + VRP state)
|
|
1010
1066
|
export function use${name}(id?: string) {
|
|
1011
1067
|
const channel = id ? \`${vrpConfig.channel}:\${id}\` : '${vrpConfig.channel}';
|
|
1012
|
-
const
|
|
1068
|
+
const vrpState = id
|
|
1013
1069
|
? useViraState<${vrpConfig.stateType}>(channel, null)
|
|
1014
1070
|
: useVrpList<${vrpConfig.stateType}>(channel);
|
|
1015
1071
|
const ${lowerName}Service = useService<{ processData: (data: ${vrpConfig.stateType} | null) => any }>('${lowerName}');
|
|
1016
1072
|
|
|
1073
|
+
// Извлекаем методы из VRP состояния
|
|
1074
|
+
const { data, sendEvent, sendDiff } = vrpState;
|
|
1075
|
+
const sendUpdate = 'sendUpdate' in vrpState ? vrpState.sendUpdate : undefined;
|
|
1076
|
+
|
|
1017
1077
|
return {
|
|
1018
1078
|
data,
|
|
1019
1079
|
// 🎯 1️⃣ Создание с авто-генерацией UUID на фронте (VRP сразу знает id, не надо ждать бэка)
|
|
1020
1080
|
create(item: Omit<${vrpConfig.stateType}, 'id' | 'created_at' | 'updated_at'>) {
|
|
1021
|
-
const
|
|
1081
|
+
const itemId = uuid();
|
|
1022
1082
|
const newItem: ${vrpConfig.stateType} = {
|
|
1023
1083
|
...item,
|
|
1024
|
-
id,
|
|
1084
|
+
id: itemId,
|
|
1025
1085
|
created_at: new Date().toISOString(),
|
|
1026
1086
|
updated_at: new Date().toISOString(),
|
|
1027
|
-
};
|
|
1087
|
+
} as ${vrpConfig.stateType};
|
|
1028
1088
|
sendEvent('${lowerName}.created', {
|
|
1029
1089
|
...newItem,
|
|
1030
1090
|
timestamp: new Date().toISOString()
|
|
@@ -1048,16 +1108,17 @@ export function use${name}(id?: string) {
|
|
|
1048
1108
|
},
|
|
1049
1109
|
sendEvent,
|
|
1050
1110
|
sendDiff,
|
|
1111
|
+
sendUpdate,
|
|
1051
1112
|
// Service methods
|
|
1052
1113
|
processData() {
|
|
1053
|
-
|
|
1114
|
+
// data может быть массивом или объектом, обрабатываем оба случая
|
|
1115
|
+
const singleData = Array.isArray(data) ? null : (data as ${vrpConfig.stateType} | null);
|
|
1116
|
+
return ${lowerName}Service.processData(singleData);
|
|
1054
1117
|
},
|
|
1055
1118
|
};
|
|
1056
1119
|
}
|
|
1057
1120
|
|
|
1058
1121
|
// 🎯 2️⃣ Сервис для bulk actions с VRP (переиспользуемый для любых сущностей)
|
|
1059
|
-
// Используем batch() из @vira-ui/core для оптимизации множественных обновлений
|
|
1060
|
-
import { batch } from '@vira-ui/core';
|
|
1061
1122
|
|
|
1062
1123
|
createService('${lowerName}Bulk', () => ({
|
|
1063
1124
|
bulkUpdate(ids: string[], payload: Partial<${vrpConfig.stateType}>, sendEvent: Function) {
|
|
@@ -1590,6 +1651,78 @@ async function generateMigration(name, dir) {
|
|
|
1590
1651
|
await fs.writeFile(upPath, upTemplate);
|
|
1591
1652
|
await fs.writeFile(downPath, downTemplate);
|
|
1592
1653
|
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Выполнение миграций через goose
|
|
1656
|
+
*/
|
|
1657
|
+
async function runMigrations(migrationsDir, dbUrl, driver, direction) {
|
|
1658
|
+
const { execSync } = require("child_process");
|
|
1659
|
+
const migrationsPath = path.resolve(process.cwd(), migrationsDir);
|
|
1660
|
+
// Проверяем наличие директории миграций
|
|
1661
|
+
if (!(await fs.pathExists(migrationsPath))) {
|
|
1662
|
+
console.error(chalk_1.default.red(`✗ Migrations directory not found: ${migrationsPath}`));
|
|
1663
|
+
console.log(chalk_1.default.yellow(` Create migrations with: npx vira make migration <name>`));
|
|
1664
|
+
process.exit(1);
|
|
1665
|
+
}
|
|
1666
|
+
// Получаем URL базы данных
|
|
1667
|
+
const databaseUrl = dbUrl || process.env.DATABASE_URL;
|
|
1668
|
+
if (!databaseUrl) {
|
|
1669
|
+
console.error(chalk_1.default.red("✗ Database URL not provided"));
|
|
1670
|
+
console.log(chalk_1.default.yellow(" Use --db-url option or set DATABASE_URL environment variable"));
|
|
1671
|
+
console.log(chalk_1.default.yellow(" Example: DATABASE_URL=postgres://user:pass@localhost/dbname?sslmode=disable"));
|
|
1672
|
+
process.exit(1);
|
|
1673
|
+
}
|
|
1674
|
+
// Проверяем наличие goose
|
|
1675
|
+
try {
|
|
1676
|
+
execSync("which goose", { stdio: "ignore" });
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
console.log(chalk_1.default.blue("Installing goose..."));
|
|
1680
|
+
try {
|
|
1681
|
+
execSync("go install github.com/pressly/goose/v3/cmd/goose@latest", { stdio: "inherit" });
|
|
1682
|
+
}
|
|
1683
|
+
catch (error) {
|
|
1684
|
+
console.error(chalk_1.default.red("✗ Failed to install goose"));
|
|
1685
|
+
console.log(chalk_1.default.yellow(" Install manually: go install github.com/pressly/goose/v3/cmd/goose@latest"));
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
// Выполняем миграции
|
|
1690
|
+
try {
|
|
1691
|
+
const command = direction === "up" ? "up" : "down";
|
|
1692
|
+
console.log(chalk_1.default.blue(`Running migrations ${direction}...`));
|
|
1693
|
+
execSync(`goose -dir "${migrationsPath}" ${driver} "${databaseUrl}" ${command}`, { stdio: "inherit", cwd: process.cwd() });
|
|
1694
|
+
console.log(chalk_1.default.green(`✓ Migrations ${direction} completed successfully`));
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
console.error(chalk_1.default.red(`✗ Migration failed: ${error.message}`));
|
|
1698
|
+
process.exit(1);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Показать статус миграций
|
|
1703
|
+
*/
|
|
1704
|
+
async function showMigrationStatus(migrationsDir, dbUrl, driver) {
|
|
1705
|
+
const { execSync } = require("child_process");
|
|
1706
|
+
const migrationsPath = path.resolve(process.cwd(), migrationsDir);
|
|
1707
|
+
if (!(await fs.pathExists(migrationsPath))) {
|
|
1708
|
+
console.error(chalk_1.default.red(`✗ Migrations directory not found: ${migrationsPath}`));
|
|
1709
|
+
process.exit(1);
|
|
1710
|
+
}
|
|
1711
|
+
const databaseUrl = dbUrl || process.env.DATABASE_URL;
|
|
1712
|
+
if (!databaseUrl) {
|
|
1713
|
+
console.error(chalk_1.default.red("✗ Database URL not provided"));
|
|
1714
|
+
console.log(chalk_1.default.yellow(" Use --db-url option or set DATABASE_URL environment variable"));
|
|
1715
|
+
process.exit(1);
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
console.log(chalk_1.default.blue("Migration status:"));
|
|
1719
|
+
execSync(`goose -dir "${migrationsPath}" ${driver} "${databaseUrl}" status`, { stdio: "inherit", cwd: process.cwd() });
|
|
1720
|
+
}
|
|
1721
|
+
catch (error) {
|
|
1722
|
+
console.error(chalk_1.default.red(`✗ Failed to get migration status: ${error.message}`));
|
|
1723
|
+
process.exit(1);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1593
1726
|
async function generateEventHandler(name, dir) {
|
|
1594
1727
|
const targetDir = path.join(process.cwd(), dir);
|
|
1595
1728
|
await fs.ensureDir(targetDir);
|
|
@@ -1643,6 +1776,29 @@ async function generateCRUDHandler(name, dir, modelName) {
|
|
|
1643
1776
|
const model = modelName || capitalize(name);
|
|
1644
1777
|
const targetDir = path.join(process.cwd(), dir);
|
|
1645
1778
|
await fs.ensureDir(targetDir);
|
|
1779
|
+
// Попытка определить модуль из go.mod
|
|
1780
|
+
let modulePath = "your-project/backend";
|
|
1781
|
+
try {
|
|
1782
|
+
// Ищем go.mod в разных возможных местах
|
|
1783
|
+
const possiblePaths = [
|
|
1784
|
+
path.join(process.cwd(), "go.mod"), // корень проекта
|
|
1785
|
+
path.join(process.cwd(), dir, "..", "..", "go.mod"), // backend/../go.mod
|
|
1786
|
+
path.join(process.cwd(), "backend", "go.mod"), // backend/go.mod
|
|
1787
|
+
];
|
|
1788
|
+
for (const goModPath of possiblePaths) {
|
|
1789
|
+
if (await fs.pathExists(goModPath)) {
|
|
1790
|
+
const goModContent = await fs.readFile(goModPath, "utf8");
|
|
1791
|
+
const moduleMatch = goModContent.match(/^module\s+(.+)$/m);
|
|
1792
|
+
if (moduleMatch) {
|
|
1793
|
+
modulePath = moduleMatch[1];
|
|
1794
|
+
break;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
catch (e) {
|
|
1800
|
+
// Игнорируем ошибки, используем дефолтный путь
|
|
1801
|
+
}
|
|
1646
1802
|
const handlerCode = `package handlers
|
|
1647
1803
|
|
|
1648
1804
|
import (
|
|
@@ -1653,6 +1809,8 @@ import (
|
|
|
1653
1809
|
"github.com/gorilla/mux"
|
|
1654
1810
|
"github.com/go-playground/validator/v10"
|
|
1655
1811
|
"github.com/google/uuid"
|
|
1812
|
+
|
|
1813
|
+
"${modulePath}/internal/models"
|
|
1656
1814
|
)
|
|
1657
1815
|
|
|
1658
1816
|
var validate = validator.New()
|
|
@@ -1666,7 +1824,7 @@ type PaginationParams struct {
|
|
|
1666
1824
|
|
|
1667
1825
|
// 🎯 Production-ready: List response with pagination
|
|
1668
1826
|
type ${handlerName}ListResponse struct {
|
|
1669
|
-
Items []
|
|
1827
|
+
Items []models.${model} \`json:"items"\`
|
|
1670
1828
|
Total int \`json:"total"\`
|
|
1671
1829
|
Limit int \`json:"limit"\`
|
|
1672
1830
|
Offset int \`json:"offset"\`
|
|
@@ -1679,8 +1837,8 @@ type ${handlerName}Event struct {
|
|
|
1679
1837
|
Type string \`json:"type"\` // created, updated, deleted
|
|
1680
1838
|
EntityID string \`json:"entity_id"\`
|
|
1681
1839
|
UserID string \`json:"user_id,omitempty"\`
|
|
1682
|
-
OldValue
|
|
1683
|
-
NewValue
|
|
1840
|
+
OldValue *models.${model} \`json:"old_value,omitempty"\`
|
|
1841
|
+
NewValue *models.${model} \`json:"new_value,omitempty"\`
|
|
1684
1842
|
Timestamp time.Time \`json:"timestamp"\`
|
|
1685
1843
|
}
|
|
1686
1844
|
|
|
@@ -1729,7 +1887,7 @@ func List${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1729
1887
|
// }
|
|
1730
1888
|
|
|
1731
1889
|
response := ${handlerName}ListResponse{
|
|
1732
|
-
Items: []
|
|
1890
|
+
Items: []models.${model}{},
|
|
1733
1891
|
Total: 0,
|
|
1734
1892
|
Limit: limit,
|
|
1735
1893
|
Offset: offset,
|
|
@@ -1763,7 +1921,7 @@ func Get${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1763
1921
|
// return
|
|
1764
1922
|
// }
|
|
1765
1923
|
|
|
1766
|
-
item :=
|
|
1924
|
+
item := models.${model}{ID: id}
|
|
1767
1925
|
|
|
1768
1926
|
// 🎯 Production-ready: Cache detail (TTL 5min)
|
|
1769
1927
|
// redis.Set(cacheKey, item, 5*time.Minute)
|
|
@@ -1773,7 +1931,7 @@ func Get${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1773
1931
|
|
|
1774
1932
|
// Create${handlerName} handles POST /${safeName}
|
|
1775
1933
|
func Create${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1776
|
-
var input
|
|
1934
|
+
var input models.${model}
|
|
1777
1935
|
|
|
1778
1936
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
1779
1937
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
@@ -1806,13 +1964,13 @@ func Create${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1806
1964
|
// }
|
|
1807
1965
|
|
|
1808
1966
|
// 🎯 Production-ready: Log event for audit trail
|
|
1809
|
-
event := ${handlerName}Event{
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1967
|
+
// event := ${handlerName}Event{
|
|
1968
|
+
// ID: uuid.New().String(),
|
|
1969
|
+
// Type: "created",
|
|
1970
|
+
// EntityID: input.ID,
|
|
1971
|
+
// NewValue: &input,
|
|
1972
|
+
// Timestamp: time.Now(),
|
|
1973
|
+
// }
|
|
1816
1974
|
// logEvent(event) // Implement event logging to client_events table
|
|
1817
1975
|
|
|
1818
1976
|
// 🎯 Production-ready: Invalidate cache
|
|
@@ -1831,7 +1989,7 @@ func Update${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1831
1989
|
vars := mux.Vars(r)
|
|
1832
1990
|
id := vars["id"]
|
|
1833
1991
|
|
|
1834
|
-
var input
|
|
1992
|
+
var input models.${model}
|
|
1835
1993
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
1836
1994
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
1837
1995
|
return
|
|
@@ -1884,7 +2042,7 @@ func Update${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1884
2042
|
// Delete${handlerName} handles DELETE /${safeName}/{id}
|
|
1885
2043
|
func Delete${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1886
2044
|
vars := mux.Vars(r)
|
|
1887
|
-
id := vars["id"]
|
|
2045
|
+
id := vars["id"] // id will be used when implementing DB delete and event logging
|
|
1888
2046
|
|
|
1889
2047
|
// 🎯 Production-ready: Get value for event logging
|
|
1890
2048
|
// oldValue, _ := db.Get${handlerName}(id)
|