@vira-ui/cli 1.1.1 → 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 +176 -26
- 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
|
* Инициализация проекта в текущей директории
|
|
@@ -292,6 +292,39 @@ make
|
|
|
292
292
|
await generateMigration(name, options.dir);
|
|
293
293
|
console.log(chalk_1.default.green(`✓ migration ${name} created in ${options.dir}`));
|
|
294
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
|
+
});
|
|
295
328
|
make
|
|
296
329
|
.command("event")
|
|
297
330
|
.description("Create Go event handler stub")
|
|
@@ -828,7 +861,29 @@ ${props.map((p) => ` ${p.name}${p.required ? "" : "?"}: ${p.type};`).join("\n")
|
|
|
828
861
|
function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
|
|
829
862
|
const propsUsage = props.map((p) => ` ${p.name}={props.${p.name}}`).join("\n");
|
|
830
863
|
if (vrpConfig) {
|
|
831
|
-
|
|
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}
|
|
832
887
|
|
|
833
888
|
// Use data from VRP state
|
|
834
889
|
// Example: const value = data?.field ?? defaultValue;
|
|
@@ -841,7 +896,7 @@ function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
|
|
|
841
896
|
// }, { debounce: 500 });
|
|
842
897
|
|
|
843
898
|
return createElement('div', { className: '${name.toLowerCase()}' },
|
|
844
|
-
|
|
899
|
+
// Add your content here
|
|
845
900
|
);`;
|
|
846
901
|
}
|
|
847
902
|
if (useViraUI) {
|
|
@@ -888,7 +943,12 @@ async function generateComponent(name, dir, config) {
|
|
|
888
943
|
`;
|
|
889
944
|
// Интерфейс состояния VRP
|
|
890
945
|
if (vrpConfig) {
|
|
891
|
-
|
|
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} {
|
|
892
952
|
// Add your state fields here
|
|
893
953
|
id?: string;
|
|
894
954
|
}
|
|
@@ -967,10 +1027,15 @@ async function collectServiceVRPConfig(name, useVRP, interactive) {
|
|
|
967
1027
|
*/
|
|
968
1028
|
function buildVRPService(name, vrpConfig) {
|
|
969
1029
|
const lowerName = name.toLowerCase();
|
|
1030
|
+
// Пытаемся определить имя типа из синхронизированных типов (например, Client вместо ClientState)
|
|
1031
|
+
const typeName = vrpConfig.stateType.replace('State', '');
|
|
970
1032
|
return `// ${name} service using Vira Core DI container + VRP
|
|
971
|
-
import { createService, useService } from '@vira-ui/core';
|
|
1033
|
+
import { createService, useService, batch } from '@vira-ui/core';
|
|
972
1034
|
import { useViraState } from '@vira-ui/react';
|
|
973
1035
|
import { v4 as uuid } from 'uuid';
|
|
1036
|
+
// TODO: Если у вас есть синхронизированные типы из backend, используйте их:
|
|
1037
|
+
// import type { ${typeName} } from '../vira-types';
|
|
1038
|
+
// export type ${vrpConfig.stateType} = ${typeName};
|
|
974
1039
|
|
|
975
1040
|
export interface ${vrpConfig.stateType} {
|
|
976
1041
|
// Add your state fields here
|
|
@@ -1000,22 +1065,26 @@ createService('${lowerName}', () => ({
|
|
|
1000
1065
|
// Hook for ${name} operations (combines service + VRP state)
|
|
1001
1066
|
export function use${name}(id?: string) {
|
|
1002
1067
|
const channel = id ? \`${vrpConfig.channel}:\${id}\` : '${vrpConfig.channel}';
|
|
1003
|
-
const
|
|
1068
|
+
const vrpState = id
|
|
1004
1069
|
? useViraState<${vrpConfig.stateType}>(channel, null)
|
|
1005
1070
|
: useVrpList<${vrpConfig.stateType}>(channel);
|
|
1006
1071
|
const ${lowerName}Service = useService<{ processData: (data: ${vrpConfig.stateType} | null) => any }>('${lowerName}');
|
|
1007
1072
|
|
|
1073
|
+
// Извлекаем методы из VRP состояния
|
|
1074
|
+
const { data, sendEvent, sendDiff } = vrpState;
|
|
1075
|
+
const sendUpdate = 'sendUpdate' in vrpState ? vrpState.sendUpdate : undefined;
|
|
1076
|
+
|
|
1008
1077
|
return {
|
|
1009
1078
|
data,
|
|
1010
1079
|
// 🎯 1️⃣ Создание с авто-генерацией UUID на фронте (VRP сразу знает id, не надо ждать бэка)
|
|
1011
1080
|
create(item: Omit<${vrpConfig.stateType}, 'id' | 'created_at' | 'updated_at'>) {
|
|
1012
|
-
const
|
|
1081
|
+
const itemId = uuid();
|
|
1013
1082
|
const newItem: ${vrpConfig.stateType} = {
|
|
1014
1083
|
...item,
|
|
1015
|
-
id,
|
|
1084
|
+
id: itemId,
|
|
1016
1085
|
created_at: new Date().toISOString(),
|
|
1017
1086
|
updated_at: new Date().toISOString(),
|
|
1018
|
-
};
|
|
1087
|
+
} as ${vrpConfig.stateType};
|
|
1019
1088
|
sendEvent('${lowerName}.created', {
|
|
1020
1089
|
...newItem,
|
|
1021
1090
|
timestamp: new Date().toISOString()
|
|
@@ -1039,16 +1108,17 @@ export function use${name}(id?: string) {
|
|
|
1039
1108
|
},
|
|
1040
1109
|
sendEvent,
|
|
1041
1110
|
sendDiff,
|
|
1111
|
+
sendUpdate,
|
|
1042
1112
|
// Service methods
|
|
1043
1113
|
processData() {
|
|
1044
|
-
|
|
1114
|
+
// data может быть массивом или объектом, обрабатываем оба случая
|
|
1115
|
+
const singleData = Array.isArray(data) ? null : (data as ${vrpConfig.stateType} | null);
|
|
1116
|
+
return ${lowerName}Service.processData(singleData);
|
|
1045
1117
|
},
|
|
1046
1118
|
};
|
|
1047
1119
|
}
|
|
1048
1120
|
|
|
1049
1121
|
// 🎯 2️⃣ Сервис для bulk actions с VRP (переиспользуемый для любых сущностей)
|
|
1050
|
-
// Используем batch() из @vira-ui/core для оптимизации множественных обновлений
|
|
1051
|
-
import { batch } from '@vira-ui/core';
|
|
1052
1122
|
|
|
1053
1123
|
createService('${lowerName}Bulk', () => ({
|
|
1054
1124
|
bulkUpdate(ids: string[], payload: Partial<${vrpConfig.stateType}>, sendEvent: Function) {
|
|
@@ -1581,6 +1651,78 @@ async function generateMigration(name, dir) {
|
|
|
1581
1651
|
await fs.writeFile(upPath, upTemplate);
|
|
1582
1652
|
await fs.writeFile(downPath, downTemplate);
|
|
1583
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
|
+
}
|
|
1584
1726
|
async function generateEventHandler(name, dir) {
|
|
1585
1727
|
const targetDir = path.join(process.cwd(), dir);
|
|
1586
1728
|
await fs.ensureDir(targetDir);
|
|
@@ -1637,12 +1779,20 @@ async function generateCRUDHandler(name, dir, modelName) {
|
|
|
1637
1779
|
// Попытка определить модуль из go.mod
|
|
1638
1780
|
let modulePath = "your-project/backend";
|
|
1639
1781
|
try {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
+
}
|
|
1646
1796
|
}
|
|
1647
1797
|
}
|
|
1648
1798
|
}
|
|
@@ -1814,13 +1964,13 @@ func Create${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1814
1964
|
// }
|
|
1815
1965
|
|
|
1816
1966
|
// 🎯 Production-ready: Log event for audit trail
|
|
1817
|
-
event := ${handlerName}Event{
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
}
|
|
1967
|
+
// event := ${handlerName}Event{
|
|
1968
|
+
// ID: uuid.New().String(),
|
|
1969
|
+
// Type: "created",
|
|
1970
|
+
// EntityID: input.ID,
|
|
1971
|
+
// NewValue: &input,
|
|
1972
|
+
// Timestamp: time.Now(),
|
|
1973
|
+
// }
|
|
1824
1974
|
// logEvent(event) // Implement event logging to client_events table
|
|
1825
1975
|
|
|
1826
1976
|
// 🎯 Production-ready: Invalidate cache
|
|
@@ -1892,7 +2042,7 @@ func Update${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
|
1892
2042
|
// Delete${handlerName} handles DELETE /${safeName}/{id}
|
|
1893
2043
|
func Delete${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1894
2044
|
vars := mux.Vars(r)
|
|
1895
|
-
id := vars["id"]
|
|
2045
|
+
id := vars["id"] // id will be used when implementing DB delete and event logging
|
|
1896
2046
|
|
|
1897
2047
|
// 🎯 Production-ready: Get value for event logging
|
|
1898
2048
|
// oldValue, _ := db.Get${handlerName}(id)
|