@devstroupe/devkit-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.
@@ -6,7 +6,7 @@ export interface User {
6
6
  id: number;
7
7
  name: string;
8
8
  email: string;
9
- role: 'ADMIN' | 'USER';
9
+ role: any;
10
10
  avatar?: string;
11
11
  tenantId?: string;
12
12
  isActive: boolean;
@@ -95,4 +95,22 @@ export class AuthService {
95
95
  get isAuthenticated(): boolean {
96
96
  return !!this.token;
97
97
  }
98
+
99
+ hasPermission(permission: string): boolean {
100
+ const user = this.currentUser;
101
+ if (!user) return false;
102
+
103
+ const roleName = typeof user.role === 'object' ? (user.role as any).name : user.role;
104
+ const permissions = typeof user.role === 'object' ? (user.role as any).permissions : [];
105
+
106
+ if (
107
+ roleName === 'ADMIN' ||
108
+ roleName === 'Administrador' ||
109
+ (Array.isArray(permissions) && permissions.includes('*'))
110
+ ) {
111
+ return true;
112
+ }
113
+
114
+ return Array.isArray(permissions) && permissions.includes(permission);
115
+ }
98
116
  }
@@ -29,7 +29,12 @@
29
29
  "bcryptjs": "^2.4.3",
30
30
  "dotenv": "^16.4.5",
31
31
  "@devstroupe/devkit-core": "workspace:*",
32
- "@devstroupe/devkit-nest": "workspace:*"
32
+ "@devstroupe/devkit-nest": "workspace:*",
33
+ "class-validator": "^0.14.1",
34
+ "class-transformer": "^0.5.1",
35
+ "@nestjs/websockets": "^11.0.0",
36
+ "@nestjs/platform-socket.io": "^11.0.0",
37
+ "socket.io": "^4.7.5"
33
38
  },
34
39
  "devDependencies": {
35
40
  "@nestjs/cli": "^11.0.0",
@@ -20,6 +20,7 @@ if (fs.existsSync(envPath)) {
20
20
  import { NestFactory } from '@nestjs/core';
21
21
  import { AppModule } from './app.module';
22
22
  import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
23
+ import { DataSource } from 'typeorm';
23
24
 
24
25
  async function bootstrap() {
25
26
  const app = await NestFactory.create(AppModule);
@@ -39,6 +40,12 @@ async function bootstrap() {
39
40
  const document = SwaggerModule.createDocument(app, config);
40
41
  SwaggerModule.setup('docs', app, document);
41
42
 
43
+ // Executar migrations programaticamente antes de iniciar a aplicação
44
+ const dataSource = app.get(DataSource);
45
+ console.log('Executando migrations do banco de dados...');
46
+ await dataSource.runMigrations();
47
+ console.log('Migrations executadas com sucesso!');
48
+
42
49
  await app.listen(13000);
43
50
  console.log('Backend NestJS iniciado com sucesso na porta 13000!');
44
51
  console.log('Documentação Swagger disponível em: http://localhost:13000/docs');
@@ -1,3 +1,4 @@
1
+ import { IPaginator } from '@devstroupe/devkit-core';
1
2
  import { Role } from './role';
2
3
 
3
4
  export interface IRoleRepository {
@@ -6,5 +7,6 @@ export interface IRoleRepository {
6
7
  findById(id: number): Promise<Role | null>;
7
8
  findByName(name: string): Promise<Role | null>;
8
9
  findAll(): Promise<Role[]>;
10
+ findMany(query: any): Promise<IPaginator<Role>>;
9
11
  delete(id: number): Promise<void>;
10
12
  }
@@ -1,16 +1,20 @@
1
1
  import { Injectable } from '@nestjs/common';
2
2
  import { InjectRepository } from '@nestjs/typeorm';
3
3
  import { Repository } from 'typeorm';
4
+ import { BaseRepository } from '@devstroupe/devkit-nest';
5
+ import { IPaginator } from '@devstroupe/devkit-core';
4
6
  import { IRoleRepository } from '../../domain/role.repository';
5
7
  import { Role } from '../../domain/role';
6
8
  import { RoleOrmEntity } from './role.orm-entity';
7
9
 
8
10
  @Injectable()
9
- export class RoleRepositoryAdapter implements IRoleRepository {
11
+ export class RoleRepositoryAdapter extends BaseRepository<RoleOrmEntity> implements IRoleRepository {
10
12
  constructor(
11
13
  @InjectRepository(RoleOrmEntity)
12
14
  private readonly repo: Repository<RoleOrmEntity>,
13
- ) {}
15
+ ) {
16
+ super(repo);
17
+ }
14
18
 
15
19
  private toDomain(orm: RoleOrmEntity): Role {
16
20
  return {
@@ -48,6 +52,19 @@ export class RoleRepositoryAdapter implements IRoleRepository {
48
52
  return found.map(orm => this.toDomain(orm));
49
53
  }
50
54
 
55
+ async findMany(query: any): Promise<IPaginator<Role>> {
56
+ const qb = this.repo.createQueryBuilder('role');
57
+ const paginated = await this.paginate(qb, query, {
58
+ allowedFilters: ['name'],
59
+ allowedSorts: ['id', 'name'],
60
+ searchableFields: ['name'],
61
+ });
62
+ return {
63
+ items: paginated.items.map((orm) => this.toDomain(orm)),
64
+ meta: paginated.meta,
65
+ };
66
+ }
67
+
51
68
  async delete(id: number): Promise<void> {
52
69
  await this.repo.delete(id);
53
70
  }
@@ -1,4 +1,5 @@
1
1
  import { Injectable, Inject } from '@nestjs/common';
2
+ import { IPaginator } from '@devstroupe/devkit-core';
2
3
  import { IRoleRepository } from '../domain/role.repository';
3
4
  import { Role } from '../domain/role';
4
5
 
@@ -33,6 +34,10 @@ export class RoleService {
33
34
  return this.roleRepo.findAll();
34
35
  }
35
36
 
37
+ async findMany(query: any): Promise<IPaginator<Role>> {
38
+ return this.roleRepo.findMany(query);
39
+ }
40
+
36
41
  async delete(id: number): Promise<void> {
37
42
  await this.roleRepo.delete(id);
38
43
  }
@@ -12,7 +12,7 @@ export class StorageDownloadController {
12
12
  private readonly storageService: StorageService,
13
13
  ) {}
14
14
 
15
- @Get('download/:key(*)')
15
+ @Get('download/*key')
16
16
  async download(@Param('key') key: string, @Res() res: Response) {
17
17
  if (this.storageOptions.driver === 'local') {
18
18
  const storagePath = path.resolve(this.storageOptions.local?.storagePath || './storage');
@@ -1,3 +1,4 @@
1
+ import { IPaginator } from '@devstroupe/devkit-core';
1
2
  import { User } from './user';
2
3
 
3
4
  export interface IUserRepository {
@@ -6,5 +7,6 @@ export interface IUserRepository {
6
7
  findById(id: number): Promise<User | null>;
7
8
  findByEmail(email: string): Promise<User | null>;
8
9
  findAll(): Promise<User[]>;
10
+ findMany(filter: any): Promise<IPaginator<User>>;
9
11
  delete(id: number): Promise<void>;
10
12
  }
@@ -9,4 +9,6 @@ export class User {
9
9
  tenantId?: string;
10
10
  currentRefreshToken?: string;
11
11
  isActive!: boolean;
12
+ createdAt!: Date;
13
+ updatedAt!: Date;
12
14
  }
@@ -1,16 +1,20 @@
1
1
  import { Injectable } from '@nestjs/common';
2
2
  import { InjectRepository } from '@nestjs/typeorm';
3
3
  import { Repository } from 'typeorm';
4
+ import { BaseRepository } from '@devstroupe/devkit-nest';
5
+ import { IPaginator } from '@devstroupe/devkit-core';
4
6
  import { IUserRepository } from '../../domain/user.repository';
5
7
  import { User } from '../../domain/user';
6
8
  import { UserOrmEntity } from './user.orm-entity';
7
9
 
8
10
  @Injectable()
9
- export class UserRepositoryAdapter implements IUserRepository {
11
+ export class UserRepositoryAdapter extends BaseRepository<UserOrmEntity> implements IUserRepository {
10
12
  constructor(
11
13
  @InjectRepository(UserOrmEntity)
12
14
  private readonly repo: Repository<UserOrmEntity>,
13
- ) {}
15
+ ) {
16
+ super(repo);
17
+ }
14
18
 
15
19
  private toDomain(orm: UserOrmEntity): User {
16
20
  return {
@@ -22,6 +26,8 @@ export class UserRepositoryAdapter implements IUserRepository {
22
26
  tenantId: orm.tenantId,
23
27
  currentRefreshToken: orm.currentRefreshToken,
24
28
  isActive: orm.isActive,
29
+ createdAt: orm.createdAt,
30
+ updatedAt: orm.updatedAt,
25
31
  };
26
32
  }
27
33
 
@@ -53,6 +59,22 @@ export class UserRepositoryAdapter implements IUserRepository {
53
59
  return found.map((u) => this.toDomain(u));
54
60
  }
55
61
 
62
+ async findMany(filter: any): Promise<IPaginator<User>> {
63
+ const alias = 'user';
64
+ const qb = this.repo.createQueryBuilder(alias);
65
+ qb.leftJoinAndSelect(`${alias}.role`, 'role');
66
+ const paginated = await this.paginate(qb, filter, {
67
+ allowedFilters: ['name', 'email', 'isActive'],
68
+ allowedSorts: ['id', 'name', 'email'],
69
+ searchableFields: ['name', 'email'],
70
+ });
71
+
72
+ return {
73
+ items: paginated.items.map((u) => this.toDomain(u)),
74
+ meta: paginated.meta,
75
+ };
76
+ }
77
+
56
78
  async delete(id: number): Promise<void> {
57
79
  await this.repo.delete(id);
58
80
  }
@@ -1,56 +1,52 @@
1
- import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, ParseIntPipe } from '@nestjs/common';
1
+ import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Query } from '@nestjs/common';
2
2
  import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
3
3
  import { UserService } from '../../service/user.service';
4
4
  import { JwtAuthGuard } from '../../../auth/jwt-auth.guard';
5
- import { Permissions, PermissionsGuard } from '@devstroupe/devkit-nest';
5
+ import { BaseCrudController, Permissions, PermissionsGuard } from '@devstroupe/devkit-nest';
6
6
  import { User } from '../../domain/user';
7
7
 
8
8
  @ApiTags('users')
9
9
  @Controller('user')
10
10
  @UseGuards(JwtAuthGuard, PermissionsGuard)
11
11
  @ApiBearerAuth()
12
- export class UserController {
13
- constructor(private readonly userService: UserService) {}
12
+ export class UserController extends BaseCrudController<User> {
13
+ constructor(private readonly userService: UserService) {
14
+ super(userService);
15
+ }
14
16
 
15
17
  @Get()
16
18
  @Permissions('read:user')
17
- @ApiOperation({ summary: 'Listar todos os usuários (Admin)' })
19
+ @ApiOperation({ summary: 'Listar usuários com filtros e paginação' })
18
20
  @ApiResponse({ status: 200, description: 'Usuários listados com sucesso.' })
19
- async findAll() {
20
- // Retorna todos os usuários (como é para administração)
21
- // Nota: O ideal em produção seria paginar, mas para o CRUD administrativo básico serve
22
- // Vamos chamar o repositório ou expor um método de listagem.
23
- // Como o UserService atual não possui findAll, vamos buscar direto
24
- // do repositório ou implementar no UserService.
25
- return this.userService.findAll();
21
+ override async findMany(@Query() query: any): Promise<any> {
22
+ return super.findMany(query);
26
23
  }
27
24
 
28
25
  @Get(':id')
29
26
  @Permissions('read:user')
30
27
  @ApiOperation({ summary: 'Obter um usuário pelo ID' })
31
- async findOne(@Param('id', ParseIntPipe) id: number) {
32
- return this.userService.findById(id);
28
+ override async findById(@Param('id') id: string): Promise<User> {
29
+ return super.findById(id);
33
30
  }
34
31
 
35
32
  @Post()
36
33
  @Permissions('create:user')
37
34
  @ApiOperation({ summary: 'Criar um novo usuário' })
38
- async create(@Body() body: Omit<User, 'id'>) {
39
- return this.userService.create(body);
35
+ override async create(@Body() body: any): Promise<User> {
36
+ return super.create(body);
40
37
  }
41
38
 
42
39
  @Put(':id')
43
40
  @Permissions('update:user')
44
41
  @ApiOperation({ summary: 'Atualizar dados de um usuário' })
45
- async update(@Param('id', ParseIntPipe) id: number, @Body() body: Partial<User>) {
46
- return this.userService.update(id, body);
42
+ override async update(@Param('id') id: string, @Body() body: any): Promise<User> {
43
+ return super.update(id, body);
47
44
  }
48
45
 
49
46
  @Delete(':id')
50
47
  @Permissions('delete:user')
51
48
  @ApiOperation({ summary: 'Excluir um usuário' })
52
- async delete(@Param('id', ParseIntPipe) id: number) {
53
- await this.userService.delete(id);
54
- return { success: true };
49
+ override async delete(@Param('id') id: string): Promise<void> {
50
+ return super.delete(id);
55
51
  }
56
52
  }
@@ -1,5 +1,6 @@
1
1
  import { Injectable, Inject } from '@nestjs/common';
2
2
  import * as bcrypt from 'bcryptjs';
3
+ import { IPaginator } from '@devstroupe/devkit-core';
3
4
  import { IUserRepository } from '../domain/user.repository';
4
5
  import { User } from '../domain/user';
5
6
 
@@ -27,6 +28,10 @@ export class UserService {
27
28
  return this.userRepo.findById(id);
28
29
  }
29
30
 
31
+ async findOne(id: number): Promise<User | null> {
32
+ return this.findById(id);
33
+ }
34
+
30
35
  async findByEmail(email: string): Promise<User | null> {
31
36
  return this.userRepo.findByEmail(email);
32
37
  }
@@ -42,6 +47,10 @@ export class UserService {
42
47
  return this.userRepo.findAll();
43
48
  }
44
49
 
50
+ async findMany(filter: any): Promise<IPaginator<User>> {
51
+ return this.userRepo.findMany(filter);
52
+ }
53
+
45
54
  async delete(id: number): Promise<void> {
46
55
  await this.userRepo.delete(id);
47
56
  }
package/dist/index.js CHANGED
@@ -556,6 +556,32 @@ function initUi(frontendRoot, config) {
556
556
  // Atualizar roteamento para suportar MainLayoutComponent
557
557
  updateAppRoutes(frontendRoot, config);
558
558
  }
559
+ function getEntityRoutePath(entityName, config) {
560
+ const navigation = config.layout?.navigation;
561
+ if (!Array.isArray(navigation)) {
562
+ return entityName;
563
+ }
564
+ const findInMenu = (items) => {
565
+ for (const item of items) {
566
+ if (item.route) {
567
+ const routeClean = item.route.replace(/^\//, '');
568
+ if (routeClean === entityName ||
569
+ routeClean === `${entityName}s` ||
570
+ routeClean === `${entityName}es` ||
571
+ routeClean.replace(/s$/, '') === entityName) {
572
+ return routeClean;
573
+ }
574
+ }
575
+ if (item.children) {
576
+ const found = findInMenu(item.children);
577
+ if (found)
578
+ return found;
579
+ }
580
+ }
581
+ return null;
582
+ };
583
+ return findInMenu(navigation) || entityName;
584
+ }
559
585
  function updateAppRoutes(frontendRoot, config = {}) {
560
586
  const routesPath = path.join(frontendRoot, 'src', 'app', 'app.routes.ts');
561
587
  if (!fs.existsSync(routesPath)) {
@@ -596,7 +622,8 @@ function updateAppRoutes(frontendRoot, config = {}) {
596
622
  featureRoutes += `];\n`;
597
623
  fs.writeFileSync(featureRoutesPath, featureRoutes, 'utf-8');
598
624
  routeImports += `import { ${routeConstant} } from './modules/${entity.name}/${entity.name}.routes';\n`;
599
- entityRoutes += ` { path: '${entity.name}', children: ${routeConstant} },\n`;
625
+ const routePath = getEntityRoutePath(entity.name, config);
626
+ entityRoutes += ` { path: '${routePath}', children: ${routeConstant} },\n`;
600
627
  });
601
628
  const entityRoutesPath = path.join(frontendRoot, 'src', 'app', 'app.entity-routes.ts');
602
629
  fs.writeFileSync(entityRoutesPath, `import { Routes } from '@angular/router';
@@ -719,10 +746,16 @@ function ensureNavigationIcons(frontendRoot, icons) {
719
746
  content = content.replace(/(provideIcons\(\{[\s\S]*?)(\n\s*}\))/, (match, providers, suffix) => `${providers.replace(/\s*$/, '')},\n ${missingIcons.join(',\n ')}${suffix}`);
720
747
  fs.writeFileSync(configPath, content, 'utf-8');
721
748
  }
749
+ let cliVersion = '1.1.0';
750
+ try {
751
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
752
+ cliVersion = pkg.version;
753
+ }
754
+ catch { }
722
755
  program
723
756
  .name('devstroupe')
724
757
  .description('DevsTroupe Development Kit (DT-DevKit) CLI')
725
- .version('1.0.0');
758
+ .version(cliVersion);
726
759
  // Comando init
727
760
  program
728
761
  .command('init')
@@ -1036,7 +1069,7 @@ SERVICE_PORT=${port}
1036
1069
  fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
1037
1070
  fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
1038
1071
  // Domain Entity
1039
- fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, backendProperties, isTenantScoped), 'utf-8');
1072
+ fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, backendProperties, isTenantScoped, entity.automaticFields), 'utf-8');
1040
1073
  // Repository Port
1041
1074
  fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
1042
1075
  // DTO
@@ -1078,7 +1111,7 @@ SERVICE_PORT=${port}
1078
1111
  fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
1079
1112
  fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
1080
1113
  // Domain Entity
1081
- fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, entity.properties, isTenantScoped), 'utf-8');
1114
+ fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, entity.properties, isTenantScoped, entity.automaticFields), 'utf-8');
1082
1115
  // Repository Port
1083
1116
  fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
1084
1117
  // DTO
@@ -1497,6 +1530,10 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1497
1530
  // 1. db-init/init.sql
1498
1531
  const dbInitDir = path.join(projectRoot, 'db-init');
1499
1532
  fs.ensureDirSync(dbInitDir);
1533
+ try {
1534
+ fs.chmodSync(dbInitDir, 0o755);
1535
+ }
1536
+ catch { }
1500
1537
  const dbs = ['devstroupe']; // Banco principal (usado pelo gateway ou monolith)
1501
1538
  if (isMicroservices && config.entities) {
1502
1539
  config.entities.forEach((entity) => {
@@ -1504,12 +1541,18 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1504
1541
  });
1505
1542
  }
1506
1543
  const initSqlContent = dbs.map(db => `CREATE DATABASE IF NOT EXISTS \`${db}\`;`).join('\n') + '\n';
1507
- fs.writeFileSync(path.join(dbInitDir, 'init.sql'), initSqlContent, 'utf-8');
1544
+ const sqlPath = path.join(dbInitDir, 'init.sql');
1545
+ fs.writeFileSync(sqlPath, initSqlContent, 'utf-8');
1546
+ try {
1547
+ fs.chmodSync(sqlPath, 0o644);
1548
+ }
1549
+ catch { }
1508
1550
  console.log(`[Docker] db-init/init.sql gerado com sucesso.`);
1509
- // 2. docker-compose.yml
1551
+ // 2. docker-compose.yml e docker-compose.dev.yml
1510
1552
  let servicesYaml = '';
1553
+ let devServicesYaml = '';
1511
1554
  // Database service
1512
- servicesYaml += ` database:
1555
+ const dbServiceYaml = ` database:
1513
1556
  image: mysql:8.0
1514
1557
  container_name: ${projectName}-database
1515
1558
  restart: always
@@ -1521,6 +1564,8 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1521
1564
  - mysql_data:/var/lib/mysql
1522
1565
  - ./db-init:/docker-entrypoint-initdb.d
1523
1566
  `;
1567
+ servicesYaml += dbServiceYaml;
1568
+ devServicesYaml += dbServiceYaml;
1524
1569
  if (isMicroservices) {
1525
1570
  // Gateway service
1526
1571
  let gatewayEnv = ` DB_HOST: database
@@ -1554,6 +1599,20 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1554
1599
  - "13000:13000"
1555
1600
  environment:
1556
1601
  ${gatewayEnv} depends_on:
1602
+ ${gatewayDependsOn}`;
1603
+ devServicesYaml += `\n api-gateway:
1604
+ image: node:20-alpine
1605
+ container_name: ${projectName}-api-gateway-dev
1606
+ working_dir: /app
1607
+ restart: always
1608
+ volumes:
1609
+ - ./backend/api-gateway:/app
1610
+ - /app/node_modules
1611
+ ports:
1612
+ - "13000:13000"
1613
+ environment:
1614
+ ${gatewayEnv} command: sh -c "npm install && npm run start:dev"
1615
+ depends_on:
1557
1616
  ${gatewayDependsOn}`;
1558
1617
  // Containers de microsserviços
1559
1618
  if (config.entities) {
@@ -1576,6 +1635,27 @@ ${gatewayDependsOn}`;
1576
1635
  SERVICE_PORT: ${port}
1577
1636
  depends_on:
1578
1637
  - database
1638
+ `;
1639
+ devServicesYaml += `\n\n ${entity.name}-service:
1640
+ image: node:20-alpine
1641
+ container_name: ${projectName}-${entity.name}-service-dev
1642
+ working_dir: /app
1643
+ restart: always
1644
+ volumes:
1645
+ - ./backend/services/${entity.name}:/app
1646
+ - /app/node_modules
1647
+ ports:
1648
+ - "${port}0:${port}"
1649
+ environment:
1650
+ DB_HOST: database
1651
+ DB_PORT: 3306
1652
+ DB_USER: root
1653
+ DB_PASSWORD: root
1654
+ DB_NAME: devstroupe_${entity.name.toLowerCase()}
1655
+ SERVICE_PORT: ${port}
1656
+ command: sh -c "npm install && npm run start:dev"
1657
+ depends_on:
1658
+ - database
1579
1659
  `;
1580
1660
  });
1581
1661
  }
@@ -1590,6 +1670,20 @@ ${gatewayDependsOn}`;
1590
1670
  - "14200:80"
1591
1671
  depends_on:
1592
1672
  - api-gateway
1673
+ `;
1674
+ devServicesYaml += `\n\n frontend:
1675
+ image: node:20-alpine
1676
+ container_name: ${projectName}-frontend-dev
1677
+ working_dir: /app
1678
+ restart: always
1679
+ volumes:
1680
+ - ./frontend:/app
1681
+ - /app/node_modules
1682
+ ports:
1683
+ - "14200:4200"
1684
+ command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
1685
+ depends_on:
1686
+ - api-gateway-dev
1593
1687
  `;
1594
1688
  }
1595
1689
  else {
@@ -1622,17 +1716,61 @@ ${gatewayDependsOn}`;
1622
1716
  - "14200:80"
1623
1717
  depends_on:
1624
1718
  - backend
1719
+ `;
1720
+ devServicesYaml += `\n backend:
1721
+ image: node:20-alpine
1722
+ container_name: ${projectName}-backend-dev
1723
+ working_dir: /app
1724
+ restart: always
1725
+ volumes:
1726
+ - ./backend:/app
1727
+ - /app/node_modules
1728
+ ports:
1729
+ - "13000:13000"
1730
+ environment:
1731
+ DB_HOST: database
1732
+ DB_PORT: 3306
1733
+ DB_USER: root
1734
+ DB_PASSWORD: root
1735
+ DB_NAME: devstroupe
1736
+ JWT_SECRET: devstroupe-secret-key-12345
1737
+ command: sh -c "npm install && npm run start:dev"
1738
+ depends_on:
1739
+ - database
1740
+
1741
+ frontend:
1742
+ image: node:20-alpine
1743
+ container_name: ${projectName}-frontend-dev
1744
+ working_dir: /app
1745
+ restart: always
1746
+ volumes:
1747
+ - ./frontend:/app
1748
+ - /app/node_modules
1749
+ ports:
1750
+ - "14200:4200"
1751
+ command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
1752
+ depends_on:
1753
+ - backend-dev
1625
1754
  `;
1626
1755
  }
1627
1756
  const dockerComposeContent = `version: '3.8'
1628
1757
 
1629
1758
  services:
1630
1759
  ${servicesYaml}
1760
+ volumes:
1761
+ mysql_data:
1762
+ `;
1763
+ const dockerComposeDevContent = `version: '3.8'
1764
+
1765
+ services:
1766
+ ${devServicesYaml}
1631
1767
  volumes:
1632
1768
  mysql_data:
1633
1769
  `;
1634
1770
  fs.writeFileSync(path.join(projectRoot, 'docker-compose.yml'), dockerComposeContent, 'utf-8');
1635
1771
  console.log(`[Docker] docker-compose.yml regerado com sucesso.`);
1772
+ fs.writeFileSync(path.join(projectRoot, 'docker-compose.dev.yml'), dockerComposeDevContent, 'utf-8');
1773
+ console.log(`[Docker] docker-compose.dev.yml gerado com sucesso.`);
1636
1774
  }
1637
1775
  // Comando new-project
1638
1776
  program
@@ -155,7 +155,7 @@ function runLinter(projectRoot, config) {
155
155
  // Regra: use-standard-devkit-queries (Backend)
156
156
  if (severityStandardQueries !== 'off') {
157
157
  // Controllers devem herdar de BaseCrudController
158
- if (file.endsWith('.controller.ts') && !file.endsWith('auth.controller.ts') && content.includes('@Controller') && !content.includes('extends BaseCrudController')) {
158
+ if (file.endsWith('.controller.ts') && !file.endsWith('auth.controller.ts') && !file.endsWith('storage.controller.ts') && !file.endsWith('storage-download.controller.ts') && content.includes('@Controller') && !content.includes('extends BaseCrudController')) {
159
159
  violations.push({
160
160
  file: relativePath,
161
161
  line: 1,
@@ -1,5 +1,9 @@
1
1
  import { IDevkitEntityProperty } from '@devstroupe/devkit-core';
2
- export declare function nestDomainEntityTemplate(name: string, properties: IDevkitEntityProperty[], isTenantScoped: boolean): string;
2
+ export declare function nestDomainEntityTemplate(name: string, properties: IDevkitEntityProperty[], isTenantScoped: boolean, automaticFields?: {
3
+ timestamps?: boolean;
4
+ blamers?: boolean;
5
+ optimisticLocking?: boolean;
6
+ }): string;
3
7
  export declare function nestRepositoryPortTemplate(name: string): string;
4
8
  export declare function nestOrmEntityTemplate(name: string, properties: IDevkitEntityProperty[], tableName: string, isTenantScoped: boolean, indexes?: Array<{
5
9
  columns: string[];
@@ -10,7 +10,7 @@ exports.nestControllerTemplate = nestControllerTemplate;
10
10
  exports.nestModuleTemplate = nestModuleTemplate;
11
11
  const names_1 = require("../shared/names");
12
12
  const relationships_1 = require("../shared/relationships");
13
- function nestDomainEntityTemplate(name, properties, isTenantScoped) {
13
+ function nestDomainEntityTemplate(name, properties, isTenantScoped, automaticFields) {
14
14
  const pascalName = (0, names_1.kebabToPascal)(name);
15
15
  const propsCode = properties
16
16
  .map((p) => {
@@ -38,9 +38,19 @@ function nestDomainEntityTemplate(name, properties, isTenantScoped) {
38
38
  })
39
39
  .join('\n');
40
40
  const tenantField = isTenantScoped ? '\n tenantId!: string;' : '';
41
+ let autoFieldsStr = '';
42
+ if (automaticFields?.timestamps) {
43
+ autoFieldsStr += '\n createdAt!: Date;\n updatedAt!: Date;';
44
+ }
45
+ if (automaticFields?.blamers) {
46
+ autoFieldsStr += '\n createdByUserId?: number;\n updatedByUserId?: number;';
47
+ }
48
+ if (automaticFields?.optimisticLocking) {
49
+ autoFieldsStr += '\n version!: number;';
50
+ }
41
51
  return `export class ${pascalName} {
42
52
  id!: number;
43
- ${propsCode}${tenantField}
53
+ ${propsCode}${tenantField}${autoFieldsStr}
44
54
  }
45
55
  `;
46
56
  }
@@ -74,7 +84,7 @@ function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped, inde
74
84
  typeormImports.push('JoinColumn');
75
85
  if (relationships.some((relation) => relation.kind === 'many-to-many' && relation.owner))
76
86
  typeormImports.push('JoinTable');
77
- if (indexes && indexes.length > 0)
87
+ if ((indexes && indexes.length > 0) || properties.some((p) => p.index))
78
88
  typeormImports.push('Index');
79
89
  if (persistence?.deletion === 'soft-delete')
80
90
  typeormImports.push('DeleteDateColumn');
@@ -90,6 +100,7 @@ function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped, inde
90
100
  .join('\n');
91
101
  const propsCode = properties
92
102
  .map((p) => {
103
+ const indexDecorator = p.index ? '@Index()\n ' : '';
93
104
  if (p.type === 'relationship') {
94
105
  const relation = (0, relationships_1.getRelationships)([p])[0];
95
106
  const targetPascal = (0, names_1.kebabToPascal)(relation.target);
@@ -111,11 +122,20 @@ function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped, inde
111
122
  return ` @ManyToMany(() => ${targetPascal}OrmEntity${relation.inverseSide ? inverse : ''})${joinTable}\n ${relation.relationName}?: ${targetPascal}OrmEntity[];`;
112
123
  }
113
124
  const decorator = relation.kind === 'one-to-one' ? 'OneToOne' : 'ManyToOne';
114
- const fkDecorator = p.required ? '@Column()' : '@Column({ nullable: true })';
125
+ let fkOptions = [];
126
+ if (!p.required)
127
+ fkOptions.push('nullable: true');
128
+ if (p.unique)
129
+ fkOptions.push('unique: true');
130
+ const fkDecorator = fkOptions.length > 0 ? `@Column({ ${fkOptions.join(', ')} })` : '@Column()';
115
131
  const joinColumn = relation.owner ? `\n @JoinColumn({ name: '${relation.foreignKey}', referencedColumnName: '${relation.valueField}' })` : '';
116
- return ` ${fkDecorator}\n ${relation.foreignKey}!: number;\n\n @${decorator}(() => ${targetPascal}OrmEntity${relation.inverseSide ? inverse : ''}, ${options})${joinColumn}\n ${relation.relationName}?: ${targetPascal}OrmEntity;`;
132
+ return ` ${indexDecorator}${fkDecorator}\n ${relation.foreignKey}!: number;\n\n @${decorator}(() => ${targetPascal}OrmEntity${relation.inverseSide ? inverse : ''}, ${options})${joinColumn}\n ${relation.relationName}?: ${targetPascal}OrmEntity;`;
117
133
  }
118
- let decorator = p.required ? '@Column()' : '@Column({ nullable: true })';
134
+ let colOptions = [];
135
+ if (!p.required)
136
+ colOptions.push('nullable: true');
137
+ if (p.unique)
138
+ colOptions.push('unique: true');
119
139
  let typeStr = 'string';
120
140
  const type = p.type;
121
141
  if (type === 'number' || type === 'decimal' || type === 'currency' || type === 'integer') {
@@ -123,39 +143,40 @@ function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped, inde
123
143
  if (type === 'decimal' || type === 'currency') {
124
144
  const precision = p.precision !== undefined ? p.precision : 12;
125
145
  const scale = p.scale !== undefined ? p.scale : 2;
126
- decorator = `@Column({ type: 'decimal', precision: ${precision}, scale: ${scale}${p.required ? '' : ', nullable: true'} })`;
146
+ colOptions.push(`type: 'decimal'`, `precision: ${precision}`, `scale: ${scale}`);
127
147
  }
128
148
  else if (type === 'integer') {
129
- decorator = p.required ? `@Column({ type: 'int' })` : `@Column({ type: 'int', nullable: true })`;
149
+ colOptions.push(`type: 'int'`);
130
150
  }
131
151
  }
132
152
  else if (type === 'bigint') {
133
153
  typeStr = 'string';
134
- decorator = p.required ? `@Column({ type: 'bigint' })` : `@Column({ type: 'bigint', nullable: true })`;
154
+ colOptions.push(`type: 'bigint'`);
135
155
  }
136
156
  else if (type === 'boolean') {
137
157
  typeStr = 'boolean';
138
158
  }
139
159
  else if (type === 'datepicker' || type === 'datetime') {
140
160
  typeStr = 'Date';
141
- decorator = p.required ? `@Column({ type: 'datetime' })` : `@Column({ type: 'datetime', nullable: true })`;
161
+ colOptions.push(`type: 'datetime'`);
142
162
  }
143
163
  else if (type === 'date') {
144
164
  typeStr = 'Date';
145
- decorator = p.required ? `@Column({ type: 'date' })` : `@Column({ type: 'date', nullable: true })`;
165
+ colOptions.push(`type: 'date'`);
146
166
  }
147
167
  else if (type === 'text') {
148
- decorator = `@Column({ type: 'text'${p.required ? '' : ', nullable: true'} })`;
168
+ colOptions.push(`type: 'text'`);
149
169
  }
150
170
  else if (type === 'json') {
151
171
  typeStr = 'any';
152
- decorator = p.required ? `@Column({ type: 'json' })` : `@Column({ type: 'json', nullable: true })`;
172
+ colOptions.push(`type: 'json'`);
153
173
  }
154
174
  else if (type === 'enum' && p.enumOptions) {
155
175
  typeStr = p.enumOptions.map(o => `'${o}'`).join(' | ');
156
- decorator = `@Column({ type: 'enum', enum: ${JSON.stringify(p.enumOptions)}${p.required ? '' : ', nullable: true'} })`;
176
+ colOptions.push(`type: 'enum'`, `enum: ${JSON.stringify(p.enumOptions)}`);
157
177
  }
158
- return ` ${decorator}\n ${p.name}!: ${typeStr};`;
178
+ const decorator = colOptions.length > 0 ? `@Column({ ${colOptions.join(', ')} })` : '@Column()';
179
+ return ` ${indexDecorator}${decorator}\n ${p.name}!: ${typeStr};`;
159
180
  })
160
181
  .join('\n\n');
161
182
  const tenantField = isTenantScoped ? '\n\n @Column()\n tenantId!: string;' : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devstroupe/devkit-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "DevsTroupe Development Kit CLI — scaffold NestJS+Angular projects, inject Spartan UI, generate CRUDs and audit architectural governance",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "commander": "^12.0.0",
38
38
  "fs-extra": "^11.2.0",
39
- "@devstroupe/devkit-core": "1.1.0"
39
+ "@devstroupe/devkit-core": "1.1.2"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/fs-extra": "^11.0.4",