@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.
Files changed (3) hide show
  1. package/README.md +64 -0
  2. package/dist/index.js +176 -26
  3. 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.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
- return ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);
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
- ${propsUsage ? propsUsage + ",\n" : ""} // Add your content here
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
- componentCode += `export interface ${vrpConfig.stateType} {
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 { data, sendEvent, sendUpdate, sendDiff } = id
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 id = uuid();
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
- return ${lowerName}Service.processData(data);
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
- const goModPath = path.join(process.cwd(), dir, "..", "..", "go.mod");
1641
- if (await fs.pathExists(goModPath)) {
1642
- const goModContent = await fs.readFile(goModPath, "utf8");
1643
- const moduleMatch = goModContent.match(/^module\\s+(.+)$/m);
1644
- if (moduleMatch) {
1645
- modulePath = moduleMatch[1];
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
- ID: uuid.New().String(),
1819
- Type: "created",
1820
- EntityID: input.ID,
1821
- NewValue: &input,
1822
- Timestamp: time.Now(),
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vira-ui/cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "CLI tool for ViraJS project generation",
5
5
  "author": "Vira Team",
6
6
  "license": "MIT",