@devstroupe/devkit-cli 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boilerplates/angular-template/src/app/core/services/auth.service.ts +19 -1
- package/dist/boilerplates/nest-template/package.json +6 -1
- package/dist/boilerplates/nest-template/src/main.ts +7 -0
- package/dist/boilerplates/nest-template/src/modules/role/domain/role.repository.ts +2 -0
- package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.repository.adapter.ts +19 -2
- package/dist/boilerplates/nest-template/src/modules/role/service/role.service.ts +5 -0
- package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage-download.controller.ts +1 -1
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +2 -0
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +2 -0
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +24 -2
- package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +17 -21
- package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +9 -0
- package/dist/index.js +145 -7
- package/dist/rules/linter-rules.js +1 -1
- package/dist/templates/nest/crud.templates.d.ts +5 -1
- package/dist/templates/nest/crud.templates.js +36 -15
- package/package.json +2 -2
|
@@ -6,7 +6,7 @@ export interface User {
|
|
|
6
6
|
id: number;
|
|
7
7
|
name: string;
|
|
8
8
|
email: string;
|
|
9
|
-
role:
|
|
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
|
}
|
package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.repository.adapter.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts
CHANGED
|
@@ -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,
|
|
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
|
|
19
|
+
@ApiOperation({ summary: 'Listar usuários com filtros e paginação' })
|
|
18
20
|
@ApiResponse({ status: 200, description: 'Usuários listados com sucesso.' })
|
|
19
|
-
async
|
|
20
|
-
|
|
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
|
|
32
|
-
return
|
|
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:
|
|
39
|
-
return
|
|
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'
|
|
46
|
-
return
|
|
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'
|
|
53
|
-
|
|
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
|
-
|
|
625
|
+
const routePath = getEntityRoutePath(entity.name, config);
|
|
626
|
+
entityRoutes += ` { path: '${routePath}', children: ${routeConstant} },\n`;
|
|
600
627
|
});
|
|
601
628
|
const entityRoutesPath = path.join(frontendRoot, 'src', 'app', 'app.entity-routes.ts');
|
|
602
629
|
fs.writeFileSync(entityRoutesPath, `import { Routes } from '@angular/router';
|
|
@@ -719,10 +746,16 @@ function ensureNavigationIcons(frontendRoot, icons) {
|
|
|
719
746
|
content = content.replace(/(provideIcons\(\{[\s\S]*?)(\n\s*}\))/, (match, providers, suffix) => `${providers.replace(/\s*$/, '')},\n ${missingIcons.join(',\n ')}${suffix}`);
|
|
720
747
|
fs.writeFileSync(configPath, content, 'utf-8');
|
|
721
748
|
}
|
|
749
|
+
let cliVersion = '1.1.0';
|
|
750
|
+
try {
|
|
751
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
752
|
+
cliVersion = pkg.version;
|
|
753
|
+
}
|
|
754
|
+
catch { }
|
|
722
755
|
program
|
|
723
756
|
.name('devstroupe')
|
|
724
757
|
.description('DevsTroupe Development Kit (DT-DevKit) CLI')
|
|
725
|
-
.version(
|
|
758
|
+
.version(cliVersion);
|
|
726
759
|
// Comando init
|
|
727
760
|
program
|
|
728
761
|
.command('init')
|
|
@@ -1036,7 +1069,7 @@ SERVICE_PORT=${port}
|
|
|
1036
1069
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
|
|
1037
1070
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
|
|
1038
1071
|
// Domain Entity
|
|
1039
|
-
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, backendProperties, isTenantScoped), 'utf-8');
|
|
1072
|
+
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, backendProperties, isTenantScoped, entity.automaticFields), 'utf-8');
|
|
1040
1073
|
// Repository Port
|
|
1041
1074
|
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
|
|
1042
1075
|
// DTO
|
|
@@ -1078,7 +1111,7 @@ SERVICE_PORT=${port}
|
|
|
1078
1111
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
|
|
1079
1112
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
|
|
1080
1113
|
// Domain Entity
|
|
1081
|
-
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, entity.properties, isTenantScoped), 'utf-8');
|
|
1114
|
+
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, entity.properties, isTenantScoped, entity.automaticFields), 'utf-8');
|
|
1082
1115
|
// Repository Port
|
|
1083
1116
|
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
|
|
1084
1117
|
// DTO
|
|
@@ -1497,6 +1530,10 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1497
1530
|
// 1. db-init/init.sql
|
|
1498
1531
|
const dbInitDir = path.join(projectRoot, 'db-init');
|
|
1499
1532
|
fs.ensureDirSync(dbInitDir);
|
|
1533
|
+
try {
|
|
1534
|
+
fs.chmodSync(dbInitDir, 0o755);
|
|
1535
|
+
}
|
|
1536
|
+
catch { }
|
|
1500
1537
|
const dbs = ['devstroupe']; // Banco principal (usado pelo gateway ou monolith)
|
|
1501
1538
|
if (isMicroservices && config.entities) {
|
|
1502
1539
|
config.entities.forEach((entity) => {
|
|
@@ -1504,12 +1541,18 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1504
1541
|
});
|
|
1505
1542
|
}
|
|
1506
1543
|
const initSqlContent = dbs.map(db => `CREATE DATABASE IF NOT EXISTS \`${db}\`;`).join('\n') + '\n';
|
|
1507
|
-
|
|
1544
|
+
const sqlPath = path.join(dbInitDir, 'init.sql');
|
|
1545
|
+
fs.writeFileSync(sqlPath, initSqlContent, 'utf-8');
|
|
1546
|
+
try {
|
|
1547
|
+
fs.chmodSync(sqlPath, 0o644);
|
|
1548
|
+
}
|
|
1549
|
+
catch { }
|
|
1508
1550
|
console.log(`[Docker] db-init/init.sql gerado com sucesso.`);
|
|
1509
|
-
// 2. docker-compose.yml
|
|
1551
|
+
// 2. docker-compose.yml e docker-compose.dev.yml
|
|
1510
1552
|
let servicesYaml = '';
|
|
1553
|
+
let devServicesYaml = '';
|
|
1511
1554
|
// Database service
|
|
1512
|
-
|
|
1555
|
+
const dbServiceYaml = ` database:
|
|
1513
1556
|
image: mysql:8.0
|
|
1514
1557
|
container_name: ${projectName}-database
|
|
1515
1558
|
restart: always
|
|
@@ -1521,6 +1564,8 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1521
1564
|
- mysql_data:/var/lib/mysql
|
|
1522
1565
|
- ./db-init:/docker-entrypoint-initdb.d
|
|
1523
1566
|
`;
|
|
1567
|
+
servicesYaml += dbServiceYaml;
|
|
1568
|
+
devServicesYaml += dbServiceYaml;
|
|
1524
1569
|
if (isMicroservices) {
|
|
1525
1570
|
// Gateway service
|
|
1526
1571
|
let gatewayEnv = ` DB_HOST: database
|
|
@@ -1554,6 +1599,20 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1554
1599
|
- "13000:13000"
|
|
1555
1600
|
environment:
|
|
1556
1601
|
${gatewayEnv} depends_on:
|
|
1602
|
+
${gatewayDependsOn}`;
|
|
1603
|
+
devServicesYaml += `\n api-gateway:
|
|
1604
|
+
image: node:20-alpine
|
|
1605
|
+
container_name: ${projectName}-api-gateway-dev
|
|
1606
|
+
working_dir: /app
|
|
1607
|
+
restart: always
|
|
1608
|
+
volumes:
|
|
1609
|
+
- ./backend/api-gateway:/app
|
|
1610
|
+
- /app/node_modules
|
|
1611
|
+
ports:
|
|
1612
|
+
- "13000:13000"
|
|
1613
|
+
environment:
|
|
1614
|
+
${gatewayEnv} command: sh -c "npm install && npm run start:dev"
|
|
1615
|
+
depends_on:
|
|
1557
1616
|
${gatewayDependsOn}`;
|
|
1558
1617
|
// Containers de microsserviços
|
|
1559
1618
|
if (config.entities) {
|
|
@@ -1576,6 +1635,27 @@ ${gatewayDependsOn}`;
|
|
|
1576
1635
|
SERVICE_PORT: ${port}
|
|
1577
1636
|
depends_on:
|
|
1578
1637
|
- database
|
|
1638
|
+
`;
|
|
1639
|
+
devServicesYaml += `\n\n ${entity.name}-service:
|
|
1640
|
+
image: node:20-alpine
|
|
1641
|
+
container_name: ${projectName}-${entity.name}-service-dev
|
|
1642
|
+
working_dir: /app
|
|
1643
|
+
restart: always
|
|
1644
|
+
volumes:
|
|
1645
|
+
- ./backend/services/${entity.name}:/app
|
|
1646
|
+
- /app/node_modules
|
|
1647
|
+
ports:
|
|
1648
|
+
- "${port}0:${port}"
|
|
1649
|
+
environment:
|
|
1650
|
+
DB_HOST: database
|
|
1651
|
+
DB_PORT: 3306
|
|
1652
|
+
DB_USER: root
|
|
1653
|
+
DB_PASSWORD: root
|
|
1654
|
+
DB_NAME: devstroupe_${entity.name.toLowerCase()}
|
|
1655
|
+
SERVICE_PORT: ${port}
|
|
1656
|
+
command: sh -c "npm install && npm run start:dev"
|
|
1657
|
+
depends_on:
|
|
1658
|
+
- database
|
|
1579
1659
|
`;
|
|
1580
1660
|
});
|
|
1581
1661
|
}
|
|
@@ -1590,6 +1670,20 @@ ${gatewayDependsOn}`;
|
|
|
1590
1670
|
- "14200:80"
|
|
1591
1671
|
depends_on:
|
|
1592
1672
|
- api-gateway
|
|
1673
|
+
`;
|
|
1674
|
+
devServicesYaml += `\n\n frontend:
|
|
1675
|
+
image: node:20-alpine
|
|
1676
|
+
container_name: ${projectName}-frontend-dev
|
|
1677
|
+
working_dir: /app
|
|
1678
|
+
restart: always
|
|
1679
|
+
volumes:
|
|
1680
|
+
- ./frontend:/app
|
|
1681
|
+
- /app/node_modules
|
|
1682
|
+
ports:
|
|
1683
|
+
- "14200:4200"
|
|
1684
|
+
command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
|
|
1685
|
+
depends_on:
|
|
1686
|
+
- api-gateway-dev
|
|
1593
1687
|
`;
|
|
1594
1688
|
}
|
|
1595
1689
|
else {
|
|
@@ -1622,17 +1716,61 @@ ${gatewayDependsOn}`;
|
|
|
1622
1716
|
- "14200:80"
|
|
1623
1717
|
depends_on:
|
|
1624
1718
|
- backend
|
|
1719
|
+
`;
|
|
1720
|
+
devServicesYaml += `\n backend:
|
|
1721
|
+
image: node:20-alpine
|
|
1722
|
+
container_name: ${projectName}-backend-dev
|
|
1723
|
+
working_dir: /app
|
|
1724
|
+
restart: always
|
|
1725
|
+
volumes:
|
|
1726
|
+
- ./backend:/app
|
|
1727
|
+
- /app/node_modules
|
|
1728
|
+
ports:
|
|
1729
|
+
- "13000:13000"
|
|
1730
|
+
environment:
|
|
1731
|
+
DB_HOST: database
|
|
1732
|
+
DB_PORT: 3306
|
|
1733
|
+
DB_USER: root
|
|
1734
|
+
DB_PASSWORD: root
|
|
1735
|
+
DB_NAME: devstroupe
|
|
1736
|
+
JWT_SECRET: devstroupe-secret-key-12345
|
|
1737
|
+
command: sh -c "npm install && npm run start:dev"
|
|
1738
|
+
depends_on:
|
|
1739
|
+
- database
|
|
1740
|
+
|
|
1741
|
+
frontend:
|
|
1742
|
+
image: node:20-alpine
|
|
1743
|
+
container_name: ${projectName}-frontend-dev
|
|
1744
|
+
working_dir: /app
|
|
1745
|
+
restart: always
|
|
1746
|
+
volumes:
|
|
1747
|
+
- ./frontend:/app
|
|
1748
|
+
- /app/node_modules
|
|
1749
|
+
ports:
|
|
1750
|
+
- "14200:4200"
|
|
1751
|
+
command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
|
|
1752
|
+
depends_on:
|
|
1753
|
+
- backend-dev
|
|
1625
1754
|
`;
|
|
1626
1755
|
}
|
|
1627
1756
|
const dockerComposeContent = `version: '3.8'
|
|
1628
1757
|
|
|
1629
1758
|
services:
|
|
1630
1759
|
${servicesYaml}
|
|
1760
|
+
volumes:
|
|
1761
|
+
mysql_data:
|
|
1762
|
+
`;
|
|
1763
|
+
const dockerComposeDevContent = `version: '3.8'
|
|
1764
|
+
|
|
1765
|
+
services:
|
|
1766
|
+
${devServicesYaml}
|
|
1631
1767
|
volumes:
|
|
1632
1768
|
mysql_data:
|
|
1633
1769
|
`;
|
|
1634
1770
|
fs.writeFileSync(path.join(projectRoot, 'docker-compose.yml'), dockerComposeContent, 'utf-8');
|
|
1635
1771
|
console.log(`[Docker] docker-compose.yml regerado com sucesso.`);
|
|
1772
|
+
fs.writeFileSync(path.join(projectRoot, 'docker-compose.dev.yml'), dockerComposeDevContent, 'utf-8');
|
|
1773
|
+
console.log(`[Docker] docker-compose.dev.yml gerado com sucesso.`);
|
|
1636
1774
|
}
|
|
1637
1775
|
// Comando new-project
|
|
1638
1776
|
program
|
|
@@ -155,7 +155,7 @@ function runLinter(projectRoot, config) {
|
|
|
155
155
|
// Regra: use-standard-devkit-queries (Backend)
|
|
156
156
|
if (severityStandardQueries !== 'off') {
|
|
157
157
|
// Controllers devem herdar de BaseCrudController
|
|
158
|
-
if (file.endsWith('.controller.ts') && !file.endsWith('auth.controller.ts') && content.includes('@Controller') && !content.includes('extends BaseCrudController')) {
|
|
158
|
+
if (file.endsWith('.controller.ts') && !file.endsWith('auth.controller.ts') && !file.endsWith('storage.controller.ts') && !file.endsWith('storage-download.controller.ts') && content.includes('@Controller') && !content.includes('extends BaseCrudController')) {
|
|
159
159
|
violations.push({
|
|
160
160
|
file: relativePath,
|
|
161
161
|
line: 1,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { IDevkitEntityProperty } from '@devstroupe/devkit-core';
|
|
2
|
-
export declare function nestDomainEntityTemplate(name: string, properties: IDevkitEntityProperty[], isTenantScoped: boolean
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
146
|
+
colOptions.push(`type: 'decimal'`, `precision: ${precision}`, `scale: ${scale}`);
|
|
127
147
|
}
|
|
128
148
|
else if (type === 'integer') {
|
|
129
|
-
|
|
149
|
+
colOptions.push(`type: 'int'`);
|
|
130
150
|
}
|
|
131
151
|
}
|
|
132
152
|
else if (type === 'bigint') {
|
|
133
153
|
typeStr = 'string';
|
|
134
|
-
|
|
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
|
-
|
|
161
|
+
colOptions.push(`type: 'datetime'`);
|
|
142
162
|
}
|
|
143
163
|
else if (type === 'date') {
|
|
144
164
|
typeStr = 'Date';
|
|
145
|
-
|
|
165
|
+
colOptions.push(`type: 'date'`);
|
|
146
166
|
}
|
|
147
167
|
else if (type === 'text') {
|
|
148
|
-
|
|
168
|
+
colOptions.push(`type: 'text'`);
|
|
149
169
|
}
|
|
150
170
|
else if (type === 'json') {
|
|
151
171
|
typeStr = 'any';
|
|
152
|
-
|
|
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
|
-
|
|
176
|
+
colOptions.push(`type: 'enum'`, `enum: ${JSON.stringify(p.enumOptions)}`);
|
|
157
177
|
}
|
|
158
|
-
|
|
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.
|
|
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.
|
|
39
|
+
"@devstroupe/devkit-core": "1.1.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/fs-extra": "^11.0.4",
|