@devstroupe/devkit-cli 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +138 -6
- 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';
|
|
@@ -1042,7 +1069,7 @@ SERVICE_PORT=${port}
|
|
|
1042
1069
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
|
|
1043
1070
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
|
|
1044
1071
|
// Domain Entity
|
|
1045
|
-
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, backendProperties, isTenantScoped), 'utf-8');
|
|
1072
|
+
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, backendProperties, isTenantScoped, entity.automaticFields), 'utf-8');
|
|
1046
1073
|
// Repository Port
|
|
1047
1074
|
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
|
|
1048
1075
|
// DTO
|
|
@@ -1084,7 +1111,7 @@ SERVICE_PORT=${port}
|
|
|
1084
1111
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
|
|
1085
1112
|
fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
|
|
1086
1113
|
// Domain Entity
|
|
1087
|
-
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, entity.properties, isTenantScoped), 'utf-8');
|
|
1114
|
+
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, entity.properties, isTenantScoped, entity.automaticFields), 'utf-8');
|
|
1088
1115
|
// Repository Port
|
|
1089
1116
|
fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
|
|
1090
1117
|
// DTO
|
|
@@ -1503,6 +1530,10 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1503
1530
|
// 1. db-init/init.sql
|
|
1504
1531
|
const dbInitDir = path.join(projectRoot, 'db-init');
|
|
1505
1532
|
fs.ensureDirSync(dbInitDir);
|
|
1533
|
+
try {
|
|
1534
|
+
fs.chmodSync(dbInitDir, 0o755);
|
|
1535
|
+
}
|
|
1536
|
+
catch { }
|
|
1506
1537
|
const dbs = ['devstroupe']; // Banco principal (usado pelo gateway ou monolith)
|
|
1507
1538
|
if (isMicroservices && config.entities) {
|
|
1508
1539
|
config.entities.forEach((entity) => {
|
|
@@ -1510,12 +1541,18 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1510
1541
|
});
|
|
1511
1542
|
}
|
|
1512
1543
|
const initSqlContent = dbs.map(db => `CREATE DATABASE IF NOT EXISTS \`${db}\`;`).join('\n') + '\n';
|
|
1513
|
-
|
|
1544
|
+
const sqlPath = path.join(dbInitDir, 'init.sql');
|
|
1545
|
+
fs.writeFileSync(sqlPath, initSqlContent, 'utf-8');
|
|
1546
|
+
try {
|
|
1547
|
+
fs.chmodSync(sqlPath, 0o644);
|
|
1548
|
+
}
|
|
1549
|
+
catch { }
|
|
1514
1550
|
console.log(`[Docker] db-init/init.sql gerado com sucesso.`);
|
|
1515
|
-
// 2. docker-compose.yml
|
|
1551
|
+
// 2. docker-compose.yml e docker-compose.dev.yml
|
|
1516
1552
|
let servicesYaml = '';
|
|
1553
|
+
let devServicesYaml = '';
|
|
1517
1554
|
// Database service
|
|
1518
|
-
|
|
1555
|
+
const dbServiceYaml = ` database:
|
|
1519
1556
|
image: mysql:8.0
|
|
1520
1557
|
container_name: ${projectName}-database
|
|
1521
1558
|
restart: always
|
|
@@ -1527,6 +1564,8 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1527
1564
|
- mysql_data:/var/lib/mysql
|
|
1528
1565
|
- ./db-init:/docker-entrypoint-initdb.d
|
|
1529
1566
|
`;
|
|
1567
|
+
servicesYaml += dbServiceYaml;
|
|
1568
|
+
devServicesYaml += dbServiceYaml;
|
|
1530
1569
|
if (isMicroservices) {
|
|
1531
1570
|
// Gateway service
|
|
1532
1571
|
let gatewayEnv = ` DB_HOST: database
|
|
@@ -1560,6 +1599,20 @@ function generateDockerComposeAndDbInit(projectRoot, config) {
|
|
|
1560
1599
|
- "13000:13000"
|
|
1561
1600
|
environment:
|
|
1562
1601
|
${gatewayEnv} depends_on:
|
|
1602
|
+
${gatewayDependsOn}`;
|
|
1603
|
+
devServicesYaml += `\n api-gateway:
|
|
1604
|
+
image: node:20-alpine
|
|
1605
|
+
container_name: ${projectName}-api-gateway-dev
|
|
1606
|
+
working_dir: /app
|
|
1607
|
+
restart: always
|
|
1608
|
+
volumes:
|
|
1609
|
+
- ./backend/api-gateway:/app
|
|
1610
|
+
- /app/node_modules
|
|
1611
|
+
ports:
|
|
1612
|
+
- "13000:13000"
|
|
1613
|
+
environment:
|
|
1614
|
+
${gatewayEnv} command: sh -c "npm install && npm run start:dev"
|
|
1615
|
+
depends_on:
|
|
1563
1616
|
${gatewayDependsOn}`;
|
|
1564
1617
|
// Containers de microsserviços
|
|
1565
1618
|
if (config.entities) {
|
|
@@ -1582,6 +1635,27 @@ ${gatewayDependsOn}`;
|
|
|
1582
1635
|
SERVICE_PORT: ${port}
|
|
1583
1636
|
depends_on:
|
|
1584
1637
|
- database
|
|
1638
|
+
`;
|
|
1639
|
+
devServicesYaml += `\n\n ${entity.name}-service:
|
|
1640
|
+
image: node:20-alpine
|
|
1641
|
+
container_name: ${projectName}-${entity.name}-service-dev
|
|
1642
|
+
working_dir: /app
|
|
1643
|
+
restart: always
|
|
1644
|
+
volumes:
|
|
1645
|
+
- ./backend/services/${entity.name}:/app
|
|
1646
|
+
- /app/node_modules
|
|
1647
|
+
ports:
|
|
1648
|
+
- "${port}0:${port}"
|
|
1649
|
+
environment:
|
|
1650
|
+
DB_HOST: database
|
|
1651
|
+
DB_PORT: 3306
|
|
1652
|
+
DB_USER: root
|
|
1653
|
+
DB_PASSWORD: root
|
|
1654
|
+
DB_NAME: devstroupe_${entity.name.toLowerCase()}
|
|
1655
|
+
SERVICE_PORT: ${port}
|
|
1656
|
+
command: sh -c "npm install && npm run start:dev"
|
|
1657
|
+
depends_on:
|
|
1658
|
+
- database
|
|
1585
1659
|
`;
|
|
1586
1660
|
});
|
|
1587
1661
|
}
|
|
@@ -1596,6 +1670,20 @@ ${gatewayDependsOn}`;
|
|
|
1596
1670
|
- "14200:80"
|
|
1597
1671
|
depends_on:
|
|
1598
1672
|
- api-gateway
|
|
1673
|
+
`;
|
|
1674
|
+
devServicesYaml += `\n\n frontend:
|
|
1675
|
+
image: node:20-alpine
|
|
1676
|
+
container_name: ${projectName}-frontend-dev
|
|
1677
|
+
working_dir: /app
|
|
1678
|
+
restart: always
|
|
1679
|
+
volumes:
|
|
1680
|
+
- ./frontend:/app
|
|
1681
|
+
- /app/node_modules
|
|
1682
|
+
ports:
|
|
1683
|
+
- "14200:4200"
|
|
1684
|
+
command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
|
|
1685
|
+
depends_on:
|
|
1686
|
+
- api-gateway-dev
|
|
1599
1687
|
`;
|
|
1600
1688
|
}
|
|
1601
1689
|
else {
|
|
@@ -1628,17 +1716,61 @@ ${gatewayDependsOn}`;
|
|
|
1628
1716
|
- "14200:80"
|
|
1629
1717
|
depends_on:
|
|
1630
1718
|
- backend
|
|
1719
|
+
`;
|
|
1720
|
+
devServicesYaml += `\n backend:
|
|
1721
|
+
image: node:20-alpine
|
|
1722
|
+
container_name: ${projectName}-backend-dev
|
|
1723
|
+
working_dir: /app
|
|
1724
|
+
restart: always
|
|
1725
|
+
volumes:
|
|
1726
|
+
- ./backend:/app
|
|
1727
|
+
- /app/node_modules
|
|
1728
|
+
ports:
|
|
1729
|
+
- "13000:13000"
|
|
1730
|
+
environment:
|
|
1731
|
+
DB_HOST: database
|
|
1732
|
+
DB_PORT: 3306
|
|
1733
|
+
DB_USER: root
|
|
1734
|
+
DB_PASSWORD: root
|
|
1735
|
+
DB_NAME: devstroupe
|
|
1736
|
+
JWT_SECRET: devstroupe-secret-key-12345
|
|
1737
|
+
command: sh -c "npm install && npm run start:dev"
|
|
1738
|
+
depends_on:
|
|
1739
|
+
- database
|
|
1740
|
+
|
|
1741
|
+
frontend:
|
|
1742
|
+
image: node:20-alpine
|
|
1743
|
+
container_name: ${projectName}-frontend-dev
|
|
1744
|
+
working_dir: /app
|
|
1745
|
+
restart: always
|
|
1746
|
+
volumes:
|
|
1747
|
+
- ./frontend:/app
|
|
1748
|
+
- /app/node_modules
|
|
1749
|
+
ports:
|
|
1750
|
+
- "14200:4200"
|
|
1751
|
+
command: sh -c "npm install && npm run start -- --host 0.0.0.0 --poll 2000"
|
|
1752
|
+
depends_on:
|
|
1753
|
+
- backend-dev
|
|
1631
1754
|
`;
|
|
1632
1755
|
}
|
|
1633
1756
|
const dockerComposeContent = `version: '3.8'
|
|
1634
1757
|
|
|
1635
1758
|
services:
|
|
1636
1759
|
${servicesYaml}
|
|
1760
|
+
volumes:
|
|
1761
|
+
mysql_data:
|
|
1762
|
+
`;
|
|
1763
|
+
const dockerComposeDevContent = `version: '3.8'
|
|
1764
|
+
|
|
1765
|
+
services:
|
|
1766
|
+
${devServicesYaml}
|
|
1637
1767
|
volumes:
|
|
1638
1768
|
mysql_data:
|
|
1639
1769
|
`;
|
|
1640
1770
|
fs.writeFileSync(path.join(projectRoot, 'docker-compose.yml'), dockerComposeContent, 'utf-8');
|
|
1641
1771
|
console.log(`[Docker] docker-compose.yml regerado com sucesso.`);
|
|
1772
|
+
fs.writeFileSync(path.join(projectRoot, 'docker-compose.dev.yml'), dockerComposeDevContent, 'utf-8');
|
|
1773
|
+
console.log(`[Docker] docker-compose.dev.yml gerado com sucesso.`);
|
|
1642
1774
|
}
|
|
1643
1775
|
// Comando new-project
|
|
1644
1776
|
program
|
|
@@ -155,7 +155,7 @@ function runLinter(projectRoot, config) {
|
|
|
155
155
|
// Regra: use-standard-devkit-queries (Backend)
|
|
156
156
|
if (severityStandardQueries !== 'off') {
|
|
157
157
|
// Controllers devem herdar de BaseCrudController
|
|
158
|
-
if (file.endsWith('.controller.ts') && !file.endsWith('auth.controller.ts') && content.includes('@Controller') && !content.includes('extends BaseCrudController')) {
|
|
158
|
+
if (file.endsWith('.controller.ts') && !file.endsWith('auth.controller.ts') && !file.endsWith('storage.controller.ts') && !file.endsWith('storage-download.controller.ts') && content.includes('@Controller') && !content.includes('extends BaseCrudController')) {
|
|
159
159
|
violations.push({
|
|
160
160
|
file: relativePath,
|
|
161
161
|
line: 1,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { IDevkitEntityProperty } from '@devstroupe/devkit-core';
|
|
2
|
-
export declare function nestDomainEntityTemplate(name: string, properties: IDevkitEntityProperty[], isTenantScoped: boolean
|
|
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",
|