@devstroupe/devkit-cli 1.1.1 → 1.1.3

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 (21) hide show
  1. package/dist/boilerplates/angular-template/proxy.conf.json +1 -1
  2. package/dist/boilerplates/angular-template/src/app/core/services/auth.service.ts +19 -1
  3. package/dist/boilerplates/angular-template/src/app/modules/role/role-permissions.component.ts +7 -0
  4. package/dist/boilerplates/nest-template/package.json +6 -1
  5. package/dist/boilerplates/nest-template/src/app.module.ts +3 -0
  6. package/dist/boilerplates/nest-template/src/modules/role/domain/role.repository.ts +2 -0
  7. package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.repository.adapter.ts +19 -2
  8. package/dist/boilerplates/nest-template/src/modules/role/service/role.service.ts +5 -0
  9. package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage-download.controller.ts +1 -1
  10. package/dist/boilerplates/nest-template/src/modules/user/database-seed.service.ts +17 -5
  11. package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +2 -0
  12. package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +2 -0
  13. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +24 -2
  14. package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +17 -21
  15. package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +9 -0
  16. package/dist/index.js +194 -24
  17. package/dist/rules/linter-rules.js +1 -1
  18. package/dist/templates/nest/crud.templates.d.ts +5 -1
  19. package/dist/templates/nest/crud.templates.js +36 -15
  20. package/dist/templates/ui/layout/sidebar.template.js +47 -3
  21. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "/api": {
3
- "target": "http://127.0.0.1:13000",
3
+ "target": "http://backend:13000",
4
4
  "secure": false,
5
5
  "changeOrigin": true
6
6
  }
@@ -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
  }
@@ -81,6 +81,7 @@ export class RolePermissionsComponent implements OnInit {
81
81
 
82
82
  hasPermission(action: string, resource: string): boolean {
83
83
  if (!this.selectedRole) return false;
84
+ if (this.selectedRole.permissions.includes('*')) return true;
84
85
  const permission = `${action}:${resource}`;
85
86
  return this.selectedRole.permissions.includes(permission);
86
87
  }
@@ -98,6 +99,7 @@ export class RolePermissionsComponent implements OnInit {
98
99
 
99
100
  hasSpecialPermission(permission: string): boolean {
100
101
  if (!this.selectedRole) return false;
102
+ if (this.selectedRole.permissions.includes('*')) return true;
101
103
  return this.selectedRole.permissions.includes(permission);
102
104
  }
103
105
 
@@ -118,6 +120,11 @@ export class RolePermissionsComponent implements OnInit {
118
120
  this.successMessage = '';
119
121
  this.errorMessage = '';
120
122
 
123
+ // Se for o perfil do Administrador, garantimos que a permissão curinga '*' continue salva
124
+ if (this.selectedRole.name === 'Administrador' && !this.selectedRole.permissions.includes('*')) {
125
+ this.selectedRole.permissions.push('*');
126
+ }
127
+
121
128
  this.roleService.update(this.selectedRole.id, this.selectedRole).subscribe({
122
129
  next: (updated: any) => {
123
130
  this.isSaving = false;
@@ -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",
@@ -5,6 +5,7 @@ import { UserModule } from './modules/user/user.module';
5
5
  import { AuthModule } from './modules/auth/auth.module';
6
6
  import { RoleModule } from './modules/role/role.module';
7
7
  import { StorageDownloadModule } from './modules/storage/storage.module';
8
+ import * as path from 'path';
8
9
 
9
10
  @Module({
10
11
  imports: [
@@ -17,6 +18,8 @@ import { StorageDownloadModule } from './modules/storage/storage.module';
17
18
  database: process.env.DB_NAME || 'devstroupe',
18
19
  autoLoadEntities: true,
19
20
  synchronize: false,
21
+ migrations: [path.join(__dirname, './database/migrations/*{.ts,.js}')],
22
+ migrationsRun: true,
20
23
  }),
21
24
  (StorageModule as any).registerAsync({
22
25
  useFactory: () => ({
@@ -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');
@@ -19,17 +19,25 @@ export class DatabaseSeedService implements OnApplicationBootstrap {
19
19
  try {
20
20
  const isProduction = process.env.NODE_ENV === 'production';
21
21
 
22
+ const allPermissions = [
23
+ '*',
24
+ 'read:cabin', 'create:cabin', 'update:cabin', 'delete:cabin',
25
+ 'read:company', 'create:company', 'update:company', 'delete:company',
26
+ 'read:role', 'create:role', 'update:role', 'delete:role', 'manage:permissions'
27
+ ];
28
+
22
29
  // 1. Criar Perfil Administrador se não existir
23
30
  let adminRole = await this.roleService.findByName('Administrador');
24
31
  if (!adminRole) {
25
32
  this.logger.log('Semente: Criando Perfil Administrador...');
26
33
  adminRole = await this.roleService.create({
27
34
  name: 'Administrador',
28
- permissions: [
29
- 'read:cabin', 'create:cabin', 'update:cabin', 'delete:cabin',
30
- 'read:company', 'create:company', 'update:company', 'delete:company',
31
- 'read:role', 'create:role', 'update:role', 'delete:role', 'manage:permissions'
32
- ],
35
+ permissions: allPermissions,
36
+ });
37
+ } else if (!adminRole.permissions.includes('*')) {
38
+ this.logger.log('Semente: Atualizando Perfil Administrador com curinga *...');
39
+ adminRole = await this.roleService.update(adminRole.id, {
40
+ permissions: [...new Set(['*', ...adminRole.permissions])],
33
41
  });
34
42
  }
35
43
 
@@ -61,6 +69,8 @@ export class DatabaseSeedService implements OnApplicationBootstrap {
61
69
  password: adminPassword || 'adminpassword123',
62
70
  role: adminRole,
63
71
  isActive: true,
72
+ createdAt: new Date(),
73
+ updatedAt: new Date()
64
74
  });
65
75
  this.logger.log('Semente: Usuário administrador cadastrado com sucesso!');
66
76
  }
@@ -77,6 +87,8 @@ export class DatabaseSeedService implements OnApplicationBootstrap {
77
87
  password: 'userpassword123',
78
88
  role: userRole,
79
89
  isActive: true,
90
+ createdAt: new Date(),
91
+ updatedAt: new Date()
80
92
  });
81
93
  this.logger.log('Semente: Usuário comum cadastrado com sucesso!');
82
94
  }
@@ -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
@@ -1165,6 +1192,8 @@ function patchLocalDependencies(backendDest, frontendDest, boilerplatesDir) {
1165
1192
  return;
1166
1193
  const packageJson = fs.readJsonSync(packagePath);
1167
1194
  delete packageJson.devDependencies;
1195
+ delete packageJson.peerDependencies;
1196
+ delete packageJson.dependencies;
1168
1197
  packageJson.scripts = {};
1169
1198
  fs.writeJsonSync(packagePath, packageJson, { spaces: 2 });
1170
1199
  };
@@ -1503,6 +1532,10 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1503
1532
  // 1. db-init/init.sql
1504
1533
  const dbInitDir = path.join(projectRoot, 'db-init');
1505
1534
  fs.ensureDirSync(dbInitDir);
1535
+ try {
1536
+ fs.chmodSync(dbInitDir, 0o755);
1537
+ }
1538
+ catch { }
1506
1539
  const dbs = ['devstroupe']; // Banco principal (usado pelo gateway ou monolith)
1507
1540
  if (isMicroservices && config.entities) {
1508
1541
  config.entities.forEach((entity) => {
@@ -1510,12 +1543,18 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1510
1543
  });
1511
1544
  }
1512
1545
  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');
1546
+ const sqlPath = path.join(dbInitDir, 'init.sql');
1547
+ fs.writeFileSync(sqlPath, initSqlContent, 'utf-8');
1548
+ try {
1549
+ fs.chmodSync(sqlPath, 0o644);
1550
+ }
1551
+ catch { }
1514
1552
  console.log(`[Docker] db-init/init.sql gerado com sucesso.`);
1515
- // 2. docker-compose.yml
1553
+ // 2. docker-compose.yml e docker-compose.dev.yml
1516
1554
  let servicesYaml = '';
1555
+ let devServicesYaml = '';
1517
1556
  // Database service
1518
- servicesYaml += ` database:
1557
+ const dbServiceYaml = ` database:
1519
1558
  image: mysql:8.0
1520
1559
  container_name: ${projectName}-database
1521
1560
  restart: always
@@ -1527,6 +1566,8 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1527
1566
  - mysql_data:/var/lib/mysql
1528
1567
  - ./db-init:/docker-entrypoint-initdb.d
1529
1568
  `;
1569
+ servicesYaml += dbServiceYaml;
1570
+ devServicesYaml += dbServiceYaml;
1530
1571
  if (isMicroservices) {
1531
1572
  // Gateway service
1532
1573
  let gatewayEnv = ` DB_HOST: database
@@ -1560,6 +1601,20 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
1560
1601
  - "13000:13000"
1561
1602
  environment:
1562
1603
  ${gatewayEnv} depends_on:
1604
+ ${gatewayDependsOn}`;
1605
+ devServicesYaml += `\n api-gateway:
1606
+ image: node:20-alpine
1607
+ container_name: ${projectName}-api-gateway-dev
1608
+ working_dir: /app
1609
+ restart: always
1610
+ volumes:
1611
+ - ./backend/api-gateway:/app
1612
+ - /app/node_modules
1613
+ ports:
1614
+ - "13000:13000"
1615
+ environment:
1616
+ ${gatewayEnv} command: sh -c "npm install && npm run start:dev"
1617
+ depends_on:
1563
1618
  ${gatewayDependsOn}`;
1564
1619
  // Containers de microsserviços
1565
1620
  if (config.entities) {
@@ -1582,6 +1637,27 @@ ${gatewayDependsOn}`;
1582
1637
  SERVICE_PORT: ${port}
1583
1638
  depends_on:
1584
1639
  - database
1640
+ `;
1641
+ devServicesYaml += `\n\n ${entity.name}-service:
1642
+ image: node:20-alpine
1643
+ container_name: ${projectName}-${entity.name}-service-dev
1644
+ working_dir: /app
1645
+ restart: always
1646
+ volumes:
1647
+ - ./backend/services/${entity.name}:/app
1648
+ - /app/node_modules
1649
+ ports:
1650
+ - "${port}0:${port}"
1651
+ environment:
1652
+ DB_HOST: database
1653
+ DB_PORT: 3306
1654
+ DB_USER: root
1655
+ DB_PASSWORD: root
1656
+ DB_NAME: devstroupe_${entity.name.toLowerCase()}
1657
+ SERVICE_PORT: ${port}
1658
+ command: sh -c "npm install && npm run start:dev"
1659
+ depends_on:
1660
+ - database
1585
1661
  `;
1586
1662
  });
1587
1663
  }
@@ -1596,6 +1672,20 @@ ${gatewayDependsOn}`;
1596
1672
  - "14200:80"
1597
1673
  depends_on:
1598
1674
  - api-gateway
1675
+ `;
1676
+ devServicesYaml += `\n\n frontend:
1677
+ image: node:20-alpine
1678
+ container_name: ${projectName}-frontend-dev
1679
+ working_dir: /app
1680
+ restart: always
1681
+ volumes:
1682
+ - ./frontend:/app
1683
+ - /app/node_modules
1684
+ ports:
1685
+ - "14200:4200"
1686
+ command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
1687
+ depends_on:
1688
+ - api-gateway-dev
1599
1689
  `;
1600
1690
  }
1601
1691
  else {
@@ -1628,17 +1718,61 @@ ${gatewayDependsOn}`;
1628
1718
  - "14200:80"
1629
1719
  depends_on:
1630
1720
  - backend
1721
+ `;
1722
+ devServicesYaml += `\n backend:
1723
+ image: node:20-alpine
1724
+ container_name: ${projectName}-backend-dev
1725
+ working_dir: /app
1726
+ restart: always
1727
+ volumes:
1728
+ - ./backend:/app
1729
+ - /app/node_modules
1730
+ ports:
1731
+ - "13000:13000"
1732
+ environment:
1733
+ DB_HOST: database
1734
+ DB_PORT: 3306
1735
+ DB_USER: root
1736
+ DB_PASSWORD: root
1737
+ DB_NAME: devstroupe
1738
+ JWT_SECRET: devstroupe-secret-key-12345
1739
+ command: sh -c "npm install && npm run start:dev"
1740
+ depends_on:
1741
+ - database
1742
+
1743
+ frontend:
1744
+ image: node:20-alpine
1745
+ container_name: ${projectName}-frontend-dev
1746
+ working_dir: /app
1747
+ restart: always
1748
+ volumes:
1749
+ - ./frontend:/app
1750
+ - /app/node_modules
1751
+ ports:
1752
+ - "14200:4200"
1753
+ command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
1754
+ depends_on:
1755
+ - backend
1631
1756
  `;
1632
1757
  }
1633
1758
  const dockerComposeContent = `version: '3.8'
1634
1759
 
1635
1760
  services:
1636
1761
  ${servicesYaml}
1762
+ volumes:
1763
+ mysql_data:
1764
+ `;
1765
+ const dockerComposeDevContent = `version: '3.8'
1766
+
1767
+ services:
1768
+ ${devServicesYaml}
1637
1769
  volumes:
1638
1770
  mysql_data:
1639
1771
  `;
1640
1772
  fs.writeFileSync(path.join(projectRoot, 'docker-compose.yml'), dockerComposeContent, 'utf-8');
1641
1773
  console.log(`[Docker] docker-compose.yml regerado com sucesso.`);
1774
+ fs.writeFileSync(path.join(projectRoot, 'docker-compose.dev.yml'), dockerComposeDevContent, 'utf-8');
1775
+ console.log(`[Docker] docker-compose.dev.yml gerado com sucesso.`);
1642
1776
  }
1643
1777
  // Comando new-project
1644
1778
  program
@@ -1795,12 +1929,13 @@ R2_SECRET_KEY=
1795
1929
  // 3.2 Criar Makefile na raiz
1796
1930
  console.log('Gerando arquivo Makefile na raiz...');
1797
1931
  const backendServiceName = isMicroservices ? 'api-gateway' : 'backend';
1798
- const makefileContent = `.PHONY: up down restart build logs ps clean backend-shell frontend-shell db-shell help
1799
-
1932
+ const makefileContent = `.PHONY: up down restart build logs ps clean backend-shell frontend-shell db-shell dev-up dev-down dev-restart dev-build dev-logs dev-ps dev-clean help
1933
+
1800
1934
  DC = docker compose
1801
-
1935
+ DC_DEV = docker compose -f docker-compose.dev.yml
1936
+
1802
1937
  help:
1803
- @echo "Comandos disponíveis:"
1938
+ @echo "Comandos disponíveis (Produção):"
1804
1939
  @echo " make up Inicia os containers em background"
1805
1940
  @echo " make down Para os containers e mantém os dados"
1806
1941
  @echo " make restart Reinicia os containers"
@@ -1808,40 +1943,75 @@ help:
1808
1943
  @echo " make logs Exibe os logs em tempo real"
1809
1944
  @echo " make ps Exibe o status dos containers"
1810
1945
  @echo " make clean Para os containers e apaga todos os volumes (reseta o banco)"
1946
+ @echo ""
1947
+ @echo "Comandos disponíveis (Desenvolvimento local com Live Reload):"
1948
+ @echo " make dev-up Inicia os containers dev em background"
1949
+ @echo " make dev-down Para os containers dev e mantém os dados"
1950
+ @echo " make dev-restart Reinicia os containers dev"
1951
+ @echo " make dev-build Reconstrói as imagens dev Docker"
1952
+ @echo " make dev-logs Exibe os logs dev em tempo real"
1953
+ @echo " make dev-ps Exibe o status dos containers dev"
1954
+ @echo " make dev-clean Para os containers dev e apaga todos os volumes (reseta o banco)"
1955
+ @echo ""
1956
+ @echo "Utilitários:"
1811
1957
  @echo " make backend-shell Abre o terminal sh do container do backend"
1812
1958
  @echo " make frontend-shell Abre o terminal sh do container do frontend"
1813
1959
  @echo " make db-shell Abre o cliente mysql no container do banco"
1814
-
1960
+
1961
+ # --- Produção ---
1815
1962
  up:
1816
1963
  \$(DC) up -d
1817
-
1964
+
1818
1965
  down:
1819
1966
  \$(DC) down
1820
-
1967
+
1821
1968
  restart:
1822
1969
  \$(DC) restart
1823
-
1970
+
1824
1971
  build:
1825
1972
  \$(DC) build
1826
-
1973
+
1827
1974
  logs:
1828
1975
  \$(DC) logs -f
1829
-
1976
+
1830
1977
  ps:
1831
1978
  \$(DC) ps
1832
-
1979
+
1833
1980
  clean:
1834
1981
  \$(DC) down -v
1835
-
1982
+
1983
+ # --- Desenvolvimento ---
1984
+ dev-up:
1985
+ \$(DC_DEV) up -d
1986
+
1987
+ dev-down:
1988
+ \$(DC_DEV) down
1989
+
1990
+ dev-restart:
1991
+ \$(DC_DEV) restart
1992
+
1993
+ dev-build:
1994
+ \$(DC_DEV) build
1995
+
1996
+ dev-logs:
1997
+ \$(DC_DEV) logs -f
1998
+
1999
+ dev-ps:
2000
+ \$(DC_DEV) ps
2001
+
2002
+ dev-clean:
2003
+ \$(DC_DEV) down -v
2004
+
2005
+ # --- Utilitários ---
1836
2006
  backend-shell:
1837
- \$(DC) exec ${backendServiceName} sh
1838
-
2007
+ \$(DC_DEV) exec -it ${backendServiceName} sh || \$(DC) exec -it ${backendServiceName} sh
2008
+
1839
2009
  frontend-shell:
1840
- \$(DC) exec frontend sh
1841
-
2010
+ \$(DC_DEV) exec -it frontend sh || \$(DC) exec -it frontend sh
2011
+
1842
2012
  db-shell:
1843
- \$(DC) exec database mysql -u root -p
1844
-
2013
+ \$(DC_DEV) exec -it database mysql -u root -p || \$(DC) exec -it database mysql -u root -p
2014
+
1845
2015
  # Evita que argumentos (ex: main, staging) sejam interpretados como target
1846
2016
  %:
1847
2017
  @:
@@ -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;' : '';
@@ -3,11 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.devkitSidebarTemplate = devkitSidebarTemplate;
4
4
  function devkitSidebarTemplate() {
5
5
  return {
6
- ts: `import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
6
+ ts: `import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';
7
7
  import { CommonModule } from '@angular/common';
8
8
  import { RouterModule, Router, NavigationEnd } from '@angular/router';
9
9
  import { NgIconComponent } from '@ng-icons/core';
10
10
  import { filter } from 'rxjs/operators';
11
+ import { DEVKIT_AUTH_SERVICE } from '@devstroupe/devkit-angular';
11
12
 
12
13
  export interface INavigationItem {
13
14
  label: string;
@@ -26,13 +27,26 @@ export interface INavigationItem {
26
27
  styleUrls: ['./sidebar.component.css']
27
28
  })
28
29
  export class DevkitSidebarComponent implements OnInit {
29
- @Input() items: INavigationItem[] = [];
30
+ private _items: INavigationItem[] = [];
31
+ filteredItems: INavigationItem[] = [];
32
+
33
+ @Input() set items(value: INavigationItem[]) {
34
+ this._items = value || [];
35
+ this.filterItems();
36
+ }
37
+
38
+ get items(): INavigationItem[] {
39
+ return this._items;
40
+ }
41
+
30
42
  @Input() brandLogo = '';
31
43
  @Input() brandLogoCollapsed = '';
32
44
  @Input() brandName = '';
33
45
  @Input() isCollapsed = false;
34
46
  @Output() closeSidebar = new EventEmitter<void>();
35
47
 
48
+ private authService = inject(DEVKIT_AUTH_SERVICE, { optional: true });
49
+
36
50
  expandedItems = new Set<string>();
37
51
  currentUrl = '';
38
52
 
@@ -45,6 +59,36 @@ export class DevkitSidebarComponent implements OnInit {
45
59
  .subscribe((event: any) => {
46
60
  this.currentUrl = event.urlAfterRedirects || event.url;
47
61
  });
62
+ this.filterItems();
63
+ }
64
+
65
+ filterItems(): void {
66
+ if (!this._items) {
67
+ this.filteredItems = [];
68
+ return;
69
+ }
70
+
71
+ this.filteredItems = this._items.filter(item => {
72
+ if (!item.roles || item.roles.length === 0) {
73
+ return true;
74
+ }
75
+ if (!this.authService) {
76
+ return true;
77
+ }
78
+ return item.roles.some(role => this.authService!.hasPermission(role));
79
+ }).map(item => {
80
+ if (item.children) {
81
+ return {
82
+ ...item,
83
+ children: item.children.filter(child => {
84
+ if (!child.roles || child.roles.length === 0) return true;
85
+ if (!this.authService) return true;
86
+ return child.roles.some(role => this.authService!.hasPermission(role));
87
+ })
88
+ };
89
+ }
90
+ return item;
91
+ });
48
92
  }
49
93
 
50
94
  toggleExpand(label: string): void {
@@ -119,7 +163,7 @@ export class DevkitSidebarComponent implements OnInit {
119
163
  <!-- Menu / Navegação -->
120
164
  <nav class="dt-sidebar-nav">
121
165
  <ul class="dt-sidebar-list">
122
- <li *ngFor="let item of items">
166
+ <li *ngFor="let item of filteredItems">
123
167
 
124
168
  <!-- Link Simples (Sem Filhos) -->
125
169
  <a
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.3",
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.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/fs-extra": "^11.0.4",