@devstroupe/devkit-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.
@@ -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';
@@ -1042,7 +1069,7 @@ SERVICE_PORT=${port}
1042
1069
  fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
1043
1070
  fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
1044
1071
  // Domain Entity
1045
- 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');
1046
1073
  // Repository Port
1047
1074
  fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
1048
1075
  // DTO
@@ -1084,7 +1111,7 @@ SERVICE_PORT=${port}
1084
1111
  fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
1085
1112
  fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
1086
1113
  // Domain Entity
1087
- 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');
1088
1115
  // Repository Port
1089
1116
  fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
1090
1117
  // DTO
@@ -1503,6 +1530,10 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1503
1530
  // 1. db-init/init.sql
1504
1531
  const dbInitDir = path.join(projectRoot, 'db-init');
1505
1532
  fs.ensureDirSync(dbInitDir);
1533
+ try {
1534
+ fs.chmodSync(dbInitDir, 0o755);
1535
+ }
1536
+ catch { }
1506
1537
  const dbs = ['devstroupe']; // Banco principal (usado pelo gateway ou monolith)
1507
1538
  if (isMicroservices && config.entities) {
1508
1539
  config.entities.forEach((entity) => {
@@ -1510,12 +1541,18 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1510
1541
  });
1511
1542
  }
1512
1543
  const initSqlContent = dbs.map(db => `CREATE DATABASE IF NOT EXISTS \`${db}\`;`).join('\n') + '\n';
1513
- 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 { }
1514
1550
  console.log(`[Docker] db-init/init.sql gerado com sucesso.`);
1515
- // 2. docker-compose.yml
1551
+ // 2. docker-compose.yml e docker-compose.dev.yml
1516
1552
  let servicesYaml = '';
1553
+ let devServicesYaml = '';
1517
1554
  // Database service
1518
- servicesYaml += ` database:
1555
+ const dbServiceYaml = ` database:
1519
1556
  image: mysql:8.0
1520
1557
  container_name: ${projectName}-database
1521
1558
  restart: always
@@ -1527,6 +1564,8 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1527
1564
  - mysql_data:/var/lib/mysql
1528
1565
  - ./db-init:/docker-entrypoint-initdb.d
1529
1566
  `;
1567
+ servicesYaml += dbServiceYaml;
1568
+ devServicesYaml += dbServiceYaml;
1530
1569
  if (isMicroservices) {
1531
1570
  // Gateway service
1532
1571
  let gatewayEnv = ` DB_HOST: database
@@ -1560,6 +1599,20 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1560
1599
  - "13000:13000"
1561
1600
  environment:
1562
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:
1563
1616
  ${gatewayDependsOn}`;
1564
1617
  // Containers de microsserviços
1565
1618
  if (config.entities) {
@@ -1582,6 +1635,27 @@ ${gatewayDependsOn}`;
1582
1635
  SERVICE_PORT: ${port}
1583
1636
  depends_on:
1584
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
1585
1659
  `;
1586
1660
  });
1587
1661
  }
@@ -1596,6 +1670,20 @@ ${gatewayDependsOn}`;
1596
1670
  - "14200:80"
1597
1671
  depends_on:
1598
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
1599
1687
  `;
1600
1688
  }
1601
1689
  else {
@@ -1628,17 +1716,61 @@ ${gatewayDependsOn}`;
1628
1716
  - "14200:80"
1629
1717
  depends_on:
1630
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
1631
1754
  `;
1632
1755
  }
1633
1756
  const dockerComposeContent = `version: '3.8'
1634
1757
 
1635
1758
  services:
1636
1759
  ${servicesYaml}
1760
+ volumes:
1761
+ mysql_data:
1762
+ `;
1763
+ const dockerComposeDevContent = `version: '3.8'
1764
+
1765
+ services:
1766
+ ${devServicesYaml}
1637
1767
  volumes:
1638
1768
  mysql_data:
1639
1769
  `;
1640
1770
  fs.writeFileSync(path.join(projectRoot, 'docker-compose.yml'), dockerComposeContent, 'utf-8');
1641
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.`);
1642
1774
  }
1643
1775
  // Comando new-project
1644
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.1",
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.1"
39
+ "@devstroupe/devkit-core": "1.1.2"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/fs-extra": "^11.0.4",