@devstroupe/devkit-cli 1.0.0 → 1.1.0

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 (68) hide show
  1. package/dist/boilerplates/angular-template/node_modules/.bin/acorn +21 -0
  2. package/dist/boilerplates/angular-template/node_modules/.bin/eslint +21 -0
  3. package/dist/boilerplates/angular-template/node_modules/.bin/js-yaml +21 -0
  4. package/dist/boilerplates/angular-template/node_modules/.bin/ng +2 -2
  5. package/dist/boilerplates/angular-template/node_modules/.bin/nx +21 -0
  6. package/dist/boilerplates/angular-template/node_modules/.bin/nx-cloud +21 -0
  7. package/dist/boilerplates/angular-template/node_modules/.bin/sass +4 -4
  8. package/dist/boilerplates/angular-template/node_modules/.bin/vite +4 -4
  9. package/dist/boilerplates/angular-template/node_modules/.bin/webpack +21 -0
  10. package/dist/boilerplates/angular-template/package.json +1 -1
  11. package/dist/boilerplates/angular-template/src/app/app.config.ts +3 -0
  12. package/dist/boilerplates/angular-template/src/app/app.routes.ts +7 -0
  13. package/dist/boilerplates/angular-template/src/app/modules/role/role-permissions.component.css +38 -0
  14. package/dist/boilerplates/angular-template/src/app/modules/role/role-permissions.component.html +175 -0
  15. package/dist/boilerplates/angular-template/src/app/modules/role/role-permissions.component.ts +134 -0
  16. package/dist/boilerplates/angular-template/src/app/services/role.service.ts +12 -0
  17. package/dist/boilerplates/angular-template/src/app/services/user.service.ts +12 -0
  18. package/dist/boilerplates/angular-template/src/app/shared/components/unauthorized/unauthorized.component.css +4 -0
  19. package/dist/boilerplates/angular-template/src/app/shared/components/unauthorized/unauthorized.component.html +18 -0
  20. package/dist/boilerplates/angular-template/src/app/shared/components/unauthorized/unauthorized.component.ts +18 -0
  21. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node +21 -0
  22. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-cwd +21 -0
  23. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-esm +21 -0
  24. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-script +21 -0
  25. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-transpile-only +21 -0
  26. package/dist/boilerplates/nest-template/node_modules/.bin/ts-script +21 -0
  27. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm +2 -2
  28. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-commonjs +2 -2
  29. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-esm +2 -2
  30. package/dist/boilerplates/nest-template/package.json +1 -1
  31. package/dist/boilerplates/nest-template/src/app.module.ts +25 -0
  32. package/dist/boilerplates/nest-template/src/database/migrations/1600000000000-create-roles.ts +20 -0
  33. package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +13 -2
  34. package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +16 -0
  35. package/dist/boilerplates/nest-template/src/modules/auth/auth.module.ts +2 -0
  36. package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +54 -8
  37. package/dist/boilerplates/nest-template/src/modules/role/domain/role.repository.ts +10 -0
  38. package/dist/boilerplates/nest-template/src/modules/role/domain/role.ts +5 -0
  39. package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.orm-entity.ts +13 -0
  40. package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.repository.adapter.ts +54 -0
  41. package/dist/boilerplates/nest-template/src/modules/role/infra/http/role.controller.ts +51 -0
  42. package/dist/boilerplates/nest-template/src/modules/role/infra/http/role.dto.ts +17 -0
  43. package/dist/boilerplates/nest-template/src/modules/role/role.module.ts +20 -0
  44. package/dist/boilerplates/nest-template/src/modules/role/service/role.service.ts +39 -0
  45. package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage-download.controller.ts +38 -0
  46. package/dist/boilerplates/nest-template/src/modules/storage/storage.module.ts +7 -0
  47. package/dist/boilerplates/nest-template/src/modules/user/database-seed.service.ts +59 -18
  48. package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +1 -0
  49. package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +4 -1
  50. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +8 -3
  51. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +6 -0
  52. package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +56 -0
  53. package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +4 -0
  54. package/dist/boilerplates/nest-template/src/modules/user/user.module.ts +7 -1
  55. package/dist/index.js +24 -3
  56. package/dist/templates/angular/form.template.js +85 -9
  57. package/dist/templates/angular/list.template.js +27 -5
  58. package/dist/templates/nest/crud.templates.d.ts +16 -2
  59. package/dist/templates/nest/crud.templates.js +175 -13
  60. package/dist/templates/nest/migration.template.js +82 -6
  61. package/dist/templates/shared/relationships.js +7 -4
  62. package/dist/templates/ui/components/index.d.ts +1 -0
  63. package/dist/templates/ui/components/index.js +1 -0
  64. package/dist/templates/ui/components/input.template.js +17 -9
  65. package/dist/templates/ui/components/textarea.template.d.ts +4 -0
  66. package/dist/templates/ui/components/textarea.template.js +76 -0
  67. package/dist/templates/ui/shared/profile-component.template.js +3 -3
  68. package/package.json +3 -3
@@ -0,0 +1,20 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { TypeOrmModule } from '@nestjs/typeorm';
3
+ import { RoleOrmEntity } from './infra/database/role.orm-entity';
4
+ import { RoleController } from './infra/http/role.controller';
5
+ import { RoleService } from './service/role.service';
6
+ import { RoleRepositoryAdapter } from './infra/database/role.repository.adapter';
7
+
8
+ @Module({
9
+ imports: [TypeOrmModule.forFeature([RoleOrmEntity])],
10
+ controllers: [RoleController],
11
+ providers: [
12
+ RoleService,
13
+ {
14
+ provide: 'IRoleRepository',
15
+ useClass: RoleRepositoryAdapter,
16
+ },
17
+ ],
18
+ exports: [RoleService],
19
+ })
20
+ export class RoleModule {}
@@ -0,0 +1,39 @@
1
+ import { Injectable, Inject } from '@nestjs/common';
2
+ import { IRoleRepository } from '../domain/role.repository';
3
+ import { Role } from '../domain/role';
4
+
5
+ @Injectable()
6
+ export class RoleService {
7
+ constructor(
8
+ @Inject('IRoleRepository')
9
+ private readonly roleRepo: IRoleRepository,
10
+ ) {}
11
+
12
+ async create(role: Omit<Role, 'id'>): Promise<Role> {
13
+ const existing = await this.roleRepo.findByName(role.name);
14
+ if (existing) {
15
+ throw new Error('Role name already exists');
16
+ }
17
+ return this.roleRepo.create(role);
18
+ }
19
+
20
+ async update(id: number, role: Partial<Role>): Promise<Role> {
21
+ return this.roleRepo.update(id, role);
22
+ }
23
+
24
+ async findById(id: number): Promise<Role | null> {
25
+ return this.roleRepo.findById(id);
26
+ }
27
+
28
+ async findByName(name: string): Promise<Role | null> {
29
+ return this.roleRepo.findByName(name);
30
+ }
31
+
32
+ async findAll(): Promise<Role[]> {
33
+ return this.roleRepo.findAll();
34
+ }
35
+
36
+ async delete(id: number): Promise<void> {
37
+ await this.roleRepo.delete(id);
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import { Controller, Get, Param, Res, NotFoundException, Inject } from '@nestjs/common';
2
+ import { Response } from 'express';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { STORAGE_OPTIONS_TOKEN, StorageService } from '@devstroupe/devkit-nest';
6
+
7
+ @Controller('storage')
8
+ export class StorageDownloadController {
9
+ constructor(
10
+ @Inject(STORAGE_OPTIONS_TOKEN)
11
+ private readonly storageOptions: any,
12
+ private readonly storageService: StorageService,
13
+ ) {}
14
+
15
+ @Get('download/:key(*)')
16
+ async download(@Param('key') key: string, @Res() res: Response) {
17
+ if (this.storageOptions.driver === 'local') {
18
+ const storagePath = path.resolve(this.storageOptions.local?.storagePath || './storage');
19
+ const targetFile = path.resolve(storagePath, key);
20
+ const rootPrefix = storagePath.endsWith(path.sep) ? storagePath : storagePath + path.sep;
21
+
22
+ // Mitigação Directory Traversal
23
+ if (!targetFile.startsWith(rootPrefix)) {
24
+ throw new NotFoundException('Arquivo não encontrado ou acesso inválido.');
25
+ }
26
+
27
+ if (!fs.existsSync(targetFile)) {
28
+ throw new NotFoundException('Arquivo não encontrado.');
29
+ }
30
+
31
+ return res.sendFile(targetFile);
32
+ } else {
33
+ // S3 / R2
34
+ const url = await this.storageService.getSignedUrl(key, 300);
35
+ return res.redirect(url);
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,7 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { StorageDownloadController } from './infra/http/storage-download.controller';
3
+
4
+ @Module({
5
+ controllers: [StorageDownloadController],
6
+ })
7
+ export class StorageDownloadModule {}
@@ -1,11 +1,15 @@
1
1
  import { Injectable, OnApplicationBootstrap, Logger } from '@nestjs/common';
2
2
  import { UserService } from './service/user.service';
3
+ import { RoleService } from '../role/service/role.service';
3
4
 
4
5
  @Injectable()
5
6
  export class DatabaseSeedService implements OnApplicationBootstrap {
6
7
  private readonly logger = new Logger(DatabaseSeedService.name);
7
8
 
8
- constructor(private readonly userService: UserService) {}
9
+ constructor(
10
+ private readonly userService: UserService,
11
+ private readonly roleService: RoleService,
12
+ ) {}
9
13
 
10
14
  async onApplicationBootstrap() {
11
15
  await this.seedUsers();
@@ -13,32 +17,69 @@ export class DatabaseSeedService implements OnApplicationBootstrap {
13
17
 
14
18
  private async seedUsers() {
15
19
  try {
16
- const adminEmail = 'admin@devstroupe.com';
20
+ const isProduction = process.env.NODE_ENV === 'production';
21
+
22
+ // 1. Criar Perfil Administrador se não existir
23
+ let adminRole = await this.roleService.findByName('Administrador');
24
+ if (!adminRole) {
25
+ this.logger.log('Semente: Criando Perfil Administrador...');
26
+ adminRole = await this.roleService.create({
27
+ 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
+ ],
33
+ });
34
+ }
35
+
36
+ // 2. Criar Perfil Usuário Comum se não existir (apenas se não for produção)
37
+ let userRole = await this.roleService.findByName('Usuário Comum');
38
+ if (!userRole && !isProduction) {
39
+ this.logger.log('Semente: Criando Perfil Usuário Comum...');
40
+ userRole = await this.roleService.create({
41
+ name: 'Usuário Comum',
42
+ permissions: ['read:cabin', 'read:company'],
43
+ });
44
+ }
45
+
46
+ // 3. Admin Inicial das Envs
47
+ const adminEmail = process.env.ADMIN_EMAIL || 'admin@devstroupe.com';
48
+ const adminPassword = process.env.ADMIN_PASSWORD;
49
+
50
+ if (isProduction && !process.env.ADMIN_PASSWORD) {
51
+ this.logger.error('Semente: ERRO! A variável de ambiente ADMIN_PASSWORD precisa ser definida em produção.');
52
+ throw new Error('ADMIN_PASSWORD não configurada em produção.');
53
+ }
54
+
17
55
  const existingAdmin = await this.userService.findByEmail(adminEmail);
18
56
  if (!existingAdmin) {
19
- this.logger.log('Semente: Criando usuário administrador padrão...');
57
+ this.logger.log('Semente: Criando usuário administrador...');
20
58
  await this.userService.create({
21
- name: 'Administrador DevsTroupe',
59
+ name: 'Administrador',
22
60
  email: adminEmail,
23
- password: 'adminpassword123',
24
- role: 'ADMIN',
61
+ password: adminPassword || 'adminpassword123',
62
+ role: adminRole,
25
63
  isActive: true,
26
64
  });
27
65
  this.logger.log('Semente: Usuário administrador cadastrado com sucesso!');
28
66
  }
29
67
 
30
- const userEmail = 'user@devstroupe.com';
31
- const existingUser = await this.userService.findByEmail(userEmail);
32
- if (!existingUser) {
33
- this.logger.log('Semente: Criando usuário comum padrão...');
34
- await this.userService.create({
35
- name: 'Usuário Comum',
36
- email: userEmail,
37
- password: 'userpassword123',
38
- role: 'USER',
39
- isActive: true,
40
- });
41
- this.logger.log('Semente: Usuário comum cadastrado com sucesso!');
68
+ // 4. Criar Usuário Comum apenas se não for ambiente de produção
69
+ if (!isProduction) {
70
+ const userEmail = 'user@devstroupe.com';
71
+ const existingUser = await this.userService.findByEmail(userEmail);
72
+ if (!existingUser && userRole) {
73
+ this.logger.log('Semente: Criando usuário comum padrão...');
74
+ await this.userService.create({
75
+ name: 'Usuário Comum',
76
+ email: userEmail,
77
+ password: 'userpassword123',
78
+ role: userRole,
79
+ isActive: true,
80
+ });
81
+ this.logger.log('Semente: Usuário comum cadastrado com sucesso!');
82
+ }
42
83
  }
43
84
  } catch (error) {
44
85
  this.logger.error('Erro durante a execução do seeding de usuários:', error);
@@ -5,5 +5,6 @@ export interface IUserRepository {
5
5
  update(id: number, user: Partial<User>): Promise<User>;
6
6
  findById(id: number): Promise<User | null>;
7
7
  findByEmail(email: string): Promise<User | null>;
8
+ findAll(): Promise<User[]>;
8
9
  delete(id: number): Promise<void>;
9
10
  }
@@ -1,9 +1,12 @@
1
+ import { Role } from '../../role/domain/role';
2
+
1
3
  export class User {
2
4
  id!: number;
3
5
  name!: string;
4
6
  email!: string;
5
7
  password!: string;
6
- role!: 'ADMIN' | 'USER';
8
+ role!: Role;
7
9
  tenantId?: string;
10
+ currentRefreshToken?: string;
8
11
  isActive!: boolean;
9
12
  }
@@ -1,4 +1,5 @@
1
- import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
1
+ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
2
+ import { RoleOrmEntity } from '../../../role/infra/database/role.orm-entity';
2
3
 
3
4
  @Entity('users')
4
5
  export class UserOrmEntity {
@@ -14,12 +15,16 @@ export class UserOrmEntity {
14
15
  @Column({ type: 'varchar', length: 255 })
15
16
  password!: string;
16
17
 
17
- @Column({ type: 'varchar', length: 50, default: 'USER' })
18
- role!: 'ADMIN' | 'USER';
18
+ @ManyToOne(() => RoleOrmEntity, { nullable: false, eager: true })
19
+ @JoinColumn({ name: 'role_id' })
20
+ role!: RoleOrmEntity;
19
21
 
20
22
  @Column({ type: 'varchar', length: 255, nullable: true, name: 'tenant_id' })
21
23
  tenantId?: string;
22
24
 
25
+ @Column({ type: 'varchar', length: 500, nullable: true, name: 'current_refresh_token' })
26
+ currentRefreshToken?: string;
27
+
23
28
  @Column({ type: 'boolean', default: true, name: 'is_active' })
24
29
  isActive!: boolean;
25
30
 
@@ -20,6 +20,7 @@ export class UserRepositoryAdapter implements IUserRepository {
20
20
  password: orm.password,
21
21
  role: orm.role,
22
22
  tenantId: orm.tenantId,
23
+ currentRefreshToken: orm.currentRefreshToken,
23
24
  isActive: orm.isActive,
24
25
  };
25
26
  }
@@ -47,6 +48,11 @@ export class UserRepositoryAdapter implements IUserRepository {
47
48
  return found ? this.toDomain(found) : null;
48
49
  }
49
50
 
51
+ async findAll(): Promise<User[]> {
52
+ const found = await this.repo.find({ relations: ['role'] });
53
+ return found.map((u) => this.toDomain(u));
54
+ }
55
+
50
56
  async delete(id: number): Promise<void> {
51
57
  await this.repo.delete(id);
52
58
  }
@@ -0,0 +1,56 @@
1
+ import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, ParseIntPipe } from '@nestjs/common';
2
+ import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
3
+ import { UserService } from '../../service/user.service';
4
+ import { JwtAuthGuard } from '../../../auth/jwt-auth.guard';
5
+ import { Permissions, PermissionsGuard } from '@devstroupe/devkit-nest';
6
+ import { User } from '../../domain/user';
7
+
8
+ @ApiTags('users')
9
+ @Controller('user')
10
+ @UseGuards(JwtAuthGuard, PermissionsGuard)
11
+ @ApiBearerAuth()
12
+ export class UserController {
13
+ constructor(private readonly userService: UserService) {}
14
+
15
+ @Get()
16
+ @Permissions('read:user')
17
+ @ApiOperation({ summary: 'Listar todos os usuários (Admin)' })
18
+ @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();
26
+ }
27
+
28
+ @Get(':id')
29
+ @Permissions('read:user')
30
+ @ApiOperation({ summary: 'Obter um usuário pelo ID' })
31
+ async findOne(@Param('id', ParseIntPipe) id: number) {
32
+ return this.userService.findById(id);
33
+ }
34
+
35
+ @Post()
36
+ @Permissions('create:user')
37
+ @ApiOperation({ summary: 'Criar um novo usuário' })
38
+ async create(@Body() body: Omit<User, 'id'>) {
39
+ return this.userService.create(body);
40
+ }
41
+
42
+ @Put(':id')
43
+ @Permissions('update:user')
44
+ @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);
47
+ }
48
+
49
+ @Delete(':id')
50
+ @Permissions('delete:user')
51
+ @ApiOperation({ summary: 'Excluir um usuário' })
52
+ async delete(@Param('id', ParseIntPipe) id: number) {
53
+ await this.userService.delete(id);
54
+ return { success: true };
55
+ }
56
+ }
@@ -38,6 +38,10 @@ export class UserService {
38
38
  return this.userRepo.update(id, user);
39
39
  }
40
40
 
41
+ async findAll(): Promise<User[]> {
42
+ return this.userRepo.findAll();
43
+ }
44
+
41
45
  async delete(id: number): Promise<void> {
42
46
  await this.userRepo.delete(id);
43
47
  }
@@ -4,9 +4,15 @@ import { UserOrmEntity } from './infra/database/user.orm-entity';
4
4
  import { UserRepositoryAdapter } from './infra/database/user.repository.adapter';
5
5
  import { UserService } from './service/user.service';
6
6
  import { DatabaseSeedService } from './database-seed.service';
7
+ import { RoleModule } from '../role/role.module';
8
+ import { UserController } from './infra/http/user.controller';
7
9
 
8
10
  @Module({
9
- imports: [TypeOrmModule.forFeature([UserOrmEntity])],
11
+ imports: [
12
+ TypeOrmModule.forFeature([UserOrmEntity]),
13
+ RoleModule,
14
+ ],
15
+ controllers: [UserController],
10
16
  providers: [
11
17
  UserService,
12
18
  DatabaseSeedService,
package/dist/index.js CHANGED
@@ -450,6 +450,7 @@ function initUi(frontendRoot, config) {
450
450
  fs.ensureDirSync(path.join(targetDir, 'card-list'));
451
451
  fs.ensureDirSync(path.join(targetDir, 'simple-list'));
452
452
  fs.ensureDirSync(path.join(targetDir, 'dialog'));
453
+ fs.ensureDirSync(path.join(targetDir, 'textarea'));
453
454
  fs.ensureDirSync(path.join(targetDir, 'shell'));
454
455
  fs.ensureDirSync(path.join(targetDir, 'sidebar'));
455
456
  fs.ensureDirSync(path.join(targetDir, 'header'));
@@ -468,6 +469,10 @@ function initUi(frontendRoot, config) {
468
469
  const inp = (0, ui_templates_1.devkitInputTemplate)();
469
470
  fs.writeFileSync(path.join(targetDir, 'input', 'input.component.ts'), inp.ts, 'utf-8');
470
471
  fs.writeFileSync(path.join(targetDir, 'input', 'input.component.html'), inp.html, 'utf-8');
472
+ // 3.5. Escrever Textarea Component
473
+ const txt = (0, ui_templates_1.devkitTextareaTemplate)();
474
+ fs.writeFileSync(path.join(targetDir, 'textarea', 'textarea.component.ts'), txt.ts, 'utf-8');
475
+ fs.writeFileSync(path.join(targetDir, 'textarea', 'textarea.component.html'), txt.html, 'utf-8');
471
476
  // 4. Escrever Select Component
472
477
  const sel = (0, ui_templates_1.devkitSelectTemplate)();
473
478
  fs.writeFileSync(path.join(targetDir, 'select', 'select.component.ts'), sel.ts, 'utf-8');
@@ -1039,7 +1044,7 @@ SERVICE_PORT=${port}
1039
1044
  // Controller (TCP)
1040
1045
  fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.ms.controller.ts`), (0, cli_templates_1.nestMsControllerTemplate)(entity.name), 'utf-8');
1041
1046
  // ORM Entity
1042
- fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, backendProperties, entity.tableName || `${entity.name}s`, isTenantScoped), 'utf-8');
1047
+ fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, backendProperties, entity.tableName || `${entity.name}s`, isTenantScoped, entity.indexes, entity.persistence, entity.automaticFields), 'utf-8');
1043
1048
  // Repository Adapter
1044
1049
  fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.repository.ts`), (0, cli_templates_1.nestRepositoryAdapterTemplate)(entity.name, backendProperties, isTenantScoped), 'utf-8');
1045
1050
  // Service
@@ -1079,9 +1084,9 @@ SERVICE_PORT=${port}
1079
1084
  // DTO
1080
1085
  fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.dto.ts`), (0, cli_templates_1.nestDtoTemplate)(entity.name, entity.properties), 'utf-8');
1081
1086
  // Controller
1082
- fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.controller.ts`), (0, cli_templates_1.nestControllerTemplate)(entity.name), 'utf-8');
1087
+ fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.controller.ts`), (0, cli_templates_1.nestControllerTemplate)(entity.name, entity.persistence), 'utf-8');
1083
1088
  // ORM Entity
1084
- fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, entity.properties, entity.tableName || `${entity.name}s`, isTenantScoped), 'utf-8');
1089
+ fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, entity.properties, entity.tableName || `${entity.name}s`, isTenantScoped, entity.indexes, entity.persistence, entity.automaticFields), 'utf-8');
1085
1090
  // Repository Adapter
1086
1091
  fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.repository.ts`), (0, cli_templates_1.nestRepositoryAdapterTemplate)(entity.name, entity.properties, isTenantScoped), 'utf-8');
1087
1092
  // Service
@@ -1681,6 +1686,22 @@ DB_PORT=3306
1681
1686
  DB_USER=root
1682
1687
  DB_PASSWORD=root
1683
1688
  DB_NAME=devstroupe
1689
+
1690
+ # Configuração de Storage Nátivo
1691
+ STORAGE_DRIVER=local
1692
+ STORAGE_LOCAL_PATH=./storage
1693
+
1694
+ # Configuração AWS S3 (Opcional se STORAGE_DRIVER=s3)
1695
+ S3_BUCKET=my-bucket
1696
+ SRegion=us-east-1
1697
+ S3_ACCESS_KEY=
1698
+ S3_SECRET_KEY=
1699
+
1700
+ # Configuração Cloudflare R2 (Opcional se STORAGE_DRIVER=r2)
1701
+ R2_BUCKET=my-bucket
1702
+ R2_ACCOUNT_ID=
1703
+ R2_ACCESS_KEY=
1704
+ R2_SECRET_KEY=
1684
1705
  `;
1685
1706
  fs.writeFileSync(path.join(backendDest, '.env'), envContent, 'utf-8');
1686
1707
  fs.writeFileSync(path.join(backendDest, '.env.example'), envContent, 'utf-8');
@@ -38,13 +38,14 @@ function angularFormHtmlTemplate(name, properties, entities = []) {
38
38
  .filter((property) => property.type !== 'relationship' || (0, relationships_1.getRelationships)([property])[0].kind !== 'one-to-many')
39
39
  .map((property) => {
40
40
  const controlName = formControlName(property);
41
- if (property.type === 'boolean') {
41
+ const type = property.type;
42
+ if (type === 'boolean') {
42
43
  return ` <div *ngIf="!isFieldHidden('${controlName}')" class="flex items-center space-x-2 pt-2">
43
44
  <input type="checkbox" formControlName="${controlName}" id="${controlName}" class="h-4 w-4 rounded border-input bg-background text-primary focus:ring-ring">
44
45
  <label for="${controlName}" class="text-sm font-medium text-foreground">${property.label}</label>
45
46
  </div>`;
46
47
  }
47
- if (property.type === 'relationship') {
48
+ if (type === 'relationship') {
48
49
  const relationship = (0, relationships_1.getRelationships)([property])[0];
49
50
  const stateName = relationStateName(relationship);
50
51
  return ` <devkit-select
@@ -62,15 +63,81 @@ function angularFormHtmlTemplate(name, properties, entities = []) {
62
63
  [required]="${Boolean(property.required)}">
63
64
  </devkit-select>`;
64
65
  }
65
- const isNumber = property.type === 'number';
66
- return ` <devkit-input
66
+ if (type === 'text' || type === 'json') {
67
+ return ` <devkit-textarea
67
68
  *ngIf="!isFieldHidden('${controlName}')"
68
- ${isNumber ? 'type="number"' : ''}
69
+ formControlName="${controlName}"
70
+ label="${property.label}"
71
+ placeholder="Digite ${property.label.toLowerCase()}"
72
+ [required]="${Boolean(property.required)}">
73
+ </devkit-textarea>`;
74
+ }
75
+ if (type === 'enum') {
76
+ return ` <devkit-select
77
+ *ngIf="!isFieldHidden('${controlName}')"
78
+ formControlName="${controlName}"
79
+ label="${property.label}"
80
+ placeholder="Selecione ${property.label.toLowerCase()}"
81
+ [options]="${controlName}Options"
82
+ [required]="${Boolean(property.required)}">
83
+ </devkit-select>`;
84
+ }
85
+ if (type === 'string' ||
86
+ type === 'number' ||
87
+ type === 'decimal' ||
88
+ type === 'currency' ||
89
+ type === 'integer' ||
90
+ type === 'bigint' ||
91
+ type === 'email' ||
92
+ type === 'phone' ||
93
+ type === 'document' ||
94
+ type === 'color' ||
95
+ type === 'datepicker' ||
96
+ type === 'date' ||
97
+ type === 'datetime' ||
98
+ type === 'file') {
99
+ let typeAttr = 'type="text"';
100
+ let stepAttr = '';
101
+ let prefixAttr = '';
102
+ if (type === 'number' || type === 'integer') {
103
+ typeAttr = 'type="number"';
104
+ }
105
+ else if (type === 'decimal') {
106
+ typeAttr = 'type="number"';
107
+ stepAttr = 'step="any"';
108
+ }
109
+ else if (type === 'currency') {
110
+ typeAttr = 'type="number"';
111
+ stepAttr = 'step="0.01"';
112
+ prefixAttr = 'prefix="R$"';
113
+ }
114
+ else if (type === 'email') {
115
+ typeAttr = 'type="email"';
116
+ }
117
+ else if (type === 'color') {
118
+ typeAttr = 'type="color"';
119
+ }
120
+ else if (type === 'date' || type === 'datepicker') {
121
+ typeAttr = 'type="date"';
122
+ }
123
+ else if (type === 'datetime') {
124
+ typeAttr = 'type="datetime-local"';
125
+ }
126
+ else if (type === 'file') {
127
+ typeAttr = 'type="file"';
128
+ }
129
+ return ` <devkit-input
130
+ *ngIf="!isFieldHidden('${controlName}')"
131
+ ${typeAttr}
132
+ ${stepAttr}
133
+ ${prefixAttr}
69
134
  formControlName="${controlName}"
70
135
  label="${property.label}"
71
136
  placeholder="Digite ${property.label.toLowerCase()}"
72
137
  [required]="${Boolean(property.required)}">
73
138
  </devkit-input>`;
139
+ }
140
+ return '';
74
141
  })
75
142
  .join('\n\n');
76
143
  const dependentSections = dependentRelationships.map((relationship) => {
@@ -155,8 +222,9 @@ function angularFormTsTemplate(name, properties, entities = []) {
155
222
  const relationships = (0, relationships_1.getRelationships)(properties);
156
223
  const optionRelationships = relationships.filter((relationship) => relationship.kind !== 'one-to-many');
157
224
  const dependents = relationships.filter((relationship) => relationship.kind === 'one-to-many' && relationship.embedded);
158
- const usesInput = properties.some((property) => property.type === 'string' || property.type === 'number' || property.type === 'datepicker');
159
- const usesSelect = optionRelationships.length > 0;
225
+ const usesInput = properties.some((p) => p.type === 'string' || p.type === 'number' || p.type === 'decimal' || p.type === 'currency' || p.type === 'datepicker');
226
+ const usesTextarea = properties.some((property) => property.type === 'text');
227
+ const usesSelect = optionRelationships.length > 0 || properties.some((property) => property.type === 'enum');
160
228
  const formGroupProps = properties
161
229
  .filter((property) => property.type !== 'relationship' || (0, relationships_1.getRelationships)([property])[0].kind !== 'one-to-many')
162
230
  .map((property) => {
@@ -198,6 +266,13 @@ function angularFormTsTemplate(name, properties, entities = []) {
198
266
  minSearchLength: ${relationship.minSearchLength}
199
267
  });`;
200
268
  }).join('\n');
269
+ const enumOptionsState = properties
270
+ .filter((p) => p.type === 'enum' && p.enumOptions)
271
+ .map((p) => {
272
+ const options = p.enumOptions.map(o => ({ label: o, value: o }));
273
+ return ` readonly ${p.name}Options = ${JSON.stringify(options)};`;
274
+ })
275
+ .join('\n');
201
276
  const dependentState = dependents.map((relationship) => {
202
277
  const stateName = relationStateName(relationship);
203
278
  const columns = relationshipColumns(relationship, entities);
@@ -315,6 +390,7 @@ import { CommonModule } from '@angular/common';
315
390
  import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
316
391
  import { ActivatedRoute, Router } from '@angular/router';
317
392
  ${usesInput ? "import { DevkitInputComponent } from '@devstroupe/ui/input/input.component';" : ''}
393
+ ${usesTextarea ? "import { DevkitTextareaComponent } from '@devstroupe/ui/textarea/textarea.component';" : ''}
318
394
  ${usesSelect ? "import { DevkitSelectComponent } from '@devstroupe/ui/select/select.component';" : ''}
319
395
  import { DevkitButtonComponent } from '@devstroupe/ui/button/button.component';
320
396
  import { HlmCardImports } from '@spartan-ng/helm/card';
@@ -328,7 +404,7 @@ import { ${pascalName}Service } from '../../../services/${name}.service';
328
404
  @Component({
329
405
  selector: 'app-${name}-form',
330
406
  standalone: true,
331
- imports: [CommonModule, ReactiveFormsModule${usesInput ? ', DevkitInputComponent' : ''}${usesSelect ? ', DevkitSelectComponent' : ''}, DevkitButtonComponent, HlmCardImports, HlmSkeletonImports${componentImports.map((item) => `, ${item}`).join('')}],
407
+ imports: [CommonModule, ReactiveFormsModule${usesInput ? ', DevkitInputComponent' : ''}${usesTextarea ? ', DevkitTextareaComponent' : ''}${usesSelect ? ', DevkitSelectComponent' : ''}, DevkitButtonComponent, HlmCardImports, HlmSkeletonImports${componentImports.map((item) => `, ${item}`).join('')}],
332
408
  templateUrl: './${name}-form.component.html'
333
409
  })
334
410
  export class ${pascalName}FormComponent implements OnInit${optionRelationships.length > 0 ? ', OnDestroy' : ''} {
@@ -351,7 +427,7 @@ ${relationServiceInjections}
351
427
  isEdit = false;
352
428
  isFetching = false;
353
429
  loading = false;
354
- ${optionState}
430
+ ${optionState}${enumOptionsState ? '\n' + enumOptionsState : ''}
355
431
  ${dependentState}
356
432
 
357
433
  ngOnInit(): void {