@devstroupe/devkit-cli 1.0.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.
- package/README.md +40 -0
- package/dist/boilerplates/angular-template/.dockerignore +4 -0
- package/dist/boilerplates/angular-template/.postcssrc.json +5 -0
- package/dist/boilerplates/angular-template/Dockerfile +14 -0
- package/dist/boilerplates/angular-template/angular.json +85 -0
- package/dist/boilerplates/angular-template/components.json +5 -0
- package/dist/boilerplates/angular-template/framework.code-workspace +7 -0
- package/dist/boilerplates/angular-template/nginx.conf +23 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/browserslist +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/jiti +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/lessc +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/ng +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/ng-xi18n +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/ngc +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/sass +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/terser +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/tsc +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/tsserver +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/vite +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/vitest +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/yaml +21 -0
- package/dist/boilerplates/angular-template/package.json +47 -0
- package/dist/boilerplates/angular-template/postcss.config.js +5 -0
- package/dist/boilerplates/angular-template/proxy.conf.json +7 -0
- package/dist/boilerplates/angular-template/src/app/app.component.html +3 -0
- package/dist/boilerplates/angular-template/src/app/app.component.ts +12 -0
- package/dist/boilerplates/angular-template/src/app/app.config.ts +59 -0
- package/dist/boilerplates/angular-template/src/app/app.entity-routes.ts +4 -0
- package/dist/boilerplates/angular-template/src/app/app.routes.ts +25 -0
- package/dist/boilerplates/angular-template/src/app/core/guards/auth.guard.ts +16 -0
- package/dist/boilerplates/angular-template/src/app/core/services/auth.service.ts +98 -0
- package/dist/boilerplates/angular-template/src/app/modules/auth/login/login.component.html +51 -0
- package/dist/boilerplates/angular-template/src/app/modules/auth/login/login.component.ts +47 -0
- package/dist/boilerplates/angular-template/src/app/modules/auth/register/register.component.html +59 -0
- package/dist/boilerplates/angular-template/src/app/modules/auth/register/register.component.ts +47 -0
- package/dist/boilerplates/angular-template/src/app/modules/dashboard/dashboard.component.html +14 -0
- package/dist/boilerplates/angular-template/src/app/modules/dashboard/dashboard.component.ts +23 -0
- package/dist/boilerplates/angular-template/src/app/shared/interceptors/tenant.interceptor.ts +25 -0
- package/dist/boilerplates/angular-template/src/index.html +64 -0
- package/dist/boilerplates/angular-template/src/main.ts +6 -0
- package/dist/boilerplates/angular-template/src/styles.css +9 -0
- package/dist/boilerplates/angular-template/tsconfig.json +36 -0
- package/dist/boilerplates/nest-template/Dockerfile +16 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/acorn +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/nest +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/prettier +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/tsc +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/tsserver +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-commonjs +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-esm +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/webpack +21 -0
- package/dist/boilerplates/nest-template/package.json +43 -0
- package/dist/boilerplates/nest-template/src/app.module.ts +24 -0
- package/dist/boilerplates/nest-template/src/database/data-source.ts +16 -0
- package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +26 -0
- package/dist/boilerplates/nest-template/src/main.ts +46 -0
- package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +34 -0
- package/dist/boilerplates/nest-template/src/modules/auth/auth.module.ts +22 -0
- package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +57 -0
- package/dist/boilerplates/nest-template/src/modules/auth/current-user.decorator.ts +8 -0
- package/dist/boilerplates/nest-template/src/modules/auth/jwt-auth.guard.ts +31 -0
- package/dist/boilerplates/nest-template/src/modules/auth/roles.decorator.ts +4 -0
- package/dist/boilerplates/nest-template/src/modules/auth/roles.guard.ts +24 -0
- package/dist/boilerplates/nest-template/src/modules/user/database-seed.service.ts +47 -0
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +9 -0
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +9 -0
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +31 -0
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +53 -0
- package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +44 -0
- package/dist/boilerplates/nest-template/src/modules/user/user.module.ts +20 -0
- package/dist/boilerplates/nest-template/tsconfig.build.json +4 -0
- package/dist/boilerplates/nest-template/tsconfig.json +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1967 -0
- package/dist/migrations/sort-entities.d.ts +2 -0
- package/dist/migrations/sort-entities.js +30 -0
- package/dist/rules/linter-rules.d.ts +12 -0
- package/dist/rules/linter-rules.js +338 -0
- package/dist/rules/linter-rules.test.d.ts +1 -0
- package/dist/rules/linter-rules.test.js +214 -0
- package/dist/templates/angular/dialog-handler.template.d.ts +2 -0
- package/dist/templates/angular/dialog-handler.template.js +49 -0
- package/dist/templates/angular/form.template.d.ts +3 -0
- package/dist/templates/angular/form.template.js +453 -0
- package/dist/templates/angular/index.d.ts +4 -0
- package/dist/templates/angular/index.js +20 -0
- package/dist/templates/angular/list.template.d.ts +3 -0
- package/dist/templates/angular/list.template.js +213 -0
- package/dist/templates/angular/service.template.d.ts +1 -0
- package/dist/templates/angular/service.template.js +20 -0
- package/dist/templates/cli-templates.d.ts +2 -0
- package/dist/templates/cli-templates.js +18 -0
- package/dist/templates/nest/crud.templates.d.ts +9 -0
- package/dist/templates/nest/crud.templates.js +362 -0
- package/dist/templates/nest/index.d.ts +3 -0
- package/dist/templates/nest/index.js +19 -0
- package/dist/templates/nest/microservice.templates.d.ts +4 -0
- package/dist/templates/nest/microservice.templates.js +157 -0
- package/dist/templates/nest/migration.template.d.ts +3 -0
- package/dist/templates/nest/migration.template.js +127 -0
- package/dist/templates/relationship-templates.test.d.ts +1 -0
- package/dist/templates/relationship-templates.test.js +181 -0
- package/dist/templates/shared/names.d.ts +3 -0
- package/dist/templates/shared/names.js +14 -0
- package/dist/templates/shared/relationships.d.ts +27 -0
- package/dist/templates/shared/relationships.js +96 -0
- package/dist/templates/ui/components/avatar.template.d.ts +3 -0
- package/dist/templates/ui/components/avatar.template.js +66 -0
- package/dist/templates/ui/components/badge.template.d.ts +3 -0
- package/dist/templates/ui/components/badge.template.js +27 -0
- package/dist/templates/ui/components/button.template.d.ts +5 -0
- package/dist/templates/ui/components/button.template.js +64 -0
- package/dist/templates/ui/components/card-list.template.d.ts +5 -0
- package/dist/templates/ui/components/card-list.template.js +79 -0
- package/dist/templates/ui/components/dialog.template.d.ts +5 -0
- package/dist/templates/ui/components/dialog.template.js +51 -0
- package/dist/templates/ui/components/filter.template.d.ts +4 -0
- package/dist/templates/ui/components/filter.template.js +218 -0
- package/dist/templates/ui/components/index.d.ts +10 -0
- package/dist/templates/ui/components/index.js +26 -0
- package/dist/templates/ui/components/input.template.d.ts +4 -0
- package/dist/templates/ui/components/input.template.js +76 -0
- package/dist/templates/ui/components/select.template.d.ts +4 -0
- package/dist/templates/ui/components/select.template.js +176 -0
- package/dist/templates/ui/components/simple-list.template.d.ts +5 -0
- package/dist/templates/ui/components/simple-list.template.js +89 -0
- package/dist/templates/ui/components/table.template.d.ts +5 -0
- package/dist/templates/ui/components/table.template.js +112 -0
- package/dist/templates/ui/index.d.ts +9 -0
- package/dist/templates/ui/index.js +25 -0
- package/dist/templates/ui/layout/header.template.d.ts +5 -0
- package/dist/templates/ui/layout/header.template.js +236 -0
- package/dist/templates/ui/layout/index.d.ts +4 -0
- package/dist/templates/ui/layout/index.js +20 -0
- package/dist/templates/ui/layout/main-layout.template.d.ts +5 -0
- package/dist/templates/ui/layout/main-layout.template.js +126 -0
- package/dist/templates/ui/layout/shell.template.d.ts +5 -0
- package/dist/templates/ui/layout/shell.template.js +76 -0
- package/dist/templates/ui/layout/sidebar.template.d.ts +5 -0
- package/dist/templates/ui/layout/sidebar.template.js +412 -0
- package/dist/templates/ui/playground.template.d.ts +5 -0
- package/dist/templates/ui/playground.template.js +570 -0
- package/dist/templates/ui/readme.template.d.ts +1 -0
- package/dist/templates/ui/readme.template.js +23 -0
- package/dist/templates/ui/shared/colors.d.ts +1 -0
- package/dist/templates/ui/shared/colors.js +31 -0
- package/dist/templates/ui/shared/profile-component.template.d.ts +5 -0
- package/dist/templates/ui/shared/profile-component.template.js +403 -0
- package/dist/templates/ui/shared/profile-service.template.d.ts +1 -0
- package/dist/templates/ui/shared/profile-service.template.js +44 -0
- package/dist/templates/ui/shared/theme-service.template.d.ts +1 -0
- package/dist/templates/ui/shared/theme-service.template.js +47 -0
- package/dist/templates/ui/spartan/badge-directive.template.d.ts +3 -0
- package/dist/templates/ui/spartan/badge-directive.template.js +50 -0
- package/dist/templates/ui/spartan/button-directive.template.d.ts +3 -0
- package/dist/templates/ui/spartan/button-directive.template.js +82 -0
- package/dist/templates/ui/spartan/button-token.template.d.ts +3 -0
- package/dist/templates/ui/spartan/button-token.template.js +31 -0
- package/dist/templates/ui/spartan/hlm-core.template.d.ts +3 -0
- package/dist/templates/ui/spartan/hlm-core.template.js +322 -0
- package/dist/templates/ui/spartan/index.d.ts +5 -0
- package/dist/templates/ui/spartan/index.js +21 -0
- package/dist/templates/ui/spartan/input-directive.template.d.ts +3 -0
- package/dist/templates/ui/spartan/input-directive.template.js +31 -0
- package/dist/templates/ui/theme-css.template.d.ts +7 -0
- package/dist/templates/ui/theme-css.template.js +210 -0
- package/dist/templates/ui-templates.d.ts +1 -0
- package/dist/templates/ui-templates.js +17 -0
- package/package.json +50 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nestGatewayControllerTemplate = nestGatewayControllerTemplate;
|
|
4
|
+
exports.nestGatewayModuleTemplate = nestGatewayModuleTemplate;
|
|
5
|
+
exports.nestMsControllerTemplate = nestMsControllerTemplate;
|
|
6
|
+
exports.nestMsModuleTemplate = nestMsModuleTemplate;
|
|
7
|
+
const names_1 = require("../shared/names");
|
|
8
|
+
function nestGatewayControllerTemplate(name) {
|
|
9
|
+
const pascalName = (0, names_1.kebabToPascal)(name);
|
|
10
|
+
const serviceToken = `${name.toUpperCase()}_SERVICE`;
|
|
11
|
+
return `import { Controller, Get, Post, Put, Delete, Body, Param, Query, Inject, UseGuards, Headers } from '@nestjs/common';
|
|
12
|
+
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
|
13
|
+
import { ClientProxy } from '@nestjs/microservices';
|
|
14
|
+
import { firstValueFrom } from 'rxjs';
|
|
15
|
+
import { Create${pascalName}Dto, Update${pascalName}Dto, Filter${pascalName}Dto } from './${name}.dto';
|
|
16
|
+
import { JwtAuthGuard } from '../../../auth/jwt-auth.guard';
|
|
17
|
+
|
|
18
|
+
@Controller('${name}')
|
|
19
|
+
@ApiTags('${name}')
|
|
20
|
+
@ApiBearerAuth()
|
|
21
|
+
@UseGuards(JwtAuthGuard)
|
|
22
|
+
export class ${pascalName}GatewayController {
|
|
23
|
+
constructor(
|
|
24
|
+
@Inject('${serviceToken}') private readonly client: ClientProxy
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
@Post()
|
|
28
|
+
async create(
|
|
29
|
+
@Body() dto: Create${pascalName}Dto,
|
|
30
|
+
@Headers('x-tenant-id') tenantId: string
|
|
31
|
+
) {
|
|
32
|
+
return firstValueFrom(this.client.send({ cmd: 'create_${name}' }, { data: dto, tenantId }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Put(':id')
|
|
36
|
+
async update(
|
|
37
|
+
@Param('id') id: string,
|
|
38
|
+
@Body() dto: Update${pascalName}Dto,
|
|
39
|
+
@Headers('x-tenant-id') tenantId: string
|
|
40
|
+
) {
|
|
41
|
+
return firstValueFrom(this.client.send({ cmd: 'update_${name}' }, { id: Number(id), dto, tenantId }));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Get()
|
|
45
|
+
async findMany(
|
|
46
|
+
@Query() filter: Filter${pascalName}Dto,
|
|
47
|
+
@Headers('x-tenant-id') tenantId: string
|
|
48
|
+
) {
|
|
49
|
+
return firstValueFrom(this.client.send({ cmd: 'find_many_${name}' }, { filter, tenantId }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Get(':id')
|
|
53
|
+
async findOne(
|
|
54
|
+
@Param('id') id: string,
|
|
55
|
+
@Headers('x-tenant-id') tenantId: string
|
|
56
|
+
) {
|
|
57
|
+
return firstValueFrom(this.client.send({ cmd: 'find_one_${name}' }, { id: Number(id), tenantId }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Delete(':id')
|
|
61
|
+
async delete(
|
|
62
|
+
@Param('id') id: string,
|
|
63
|
+
@Headers('x-tenant-id') tenantId: string
|
|
64
|
+
) {
|
|
65
|
+
return firstValueFrom(this.client.send({ cmd: 'delete_${name}' }, { id: Number(id), tenantId }));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function nestGatewayModuleTemplate(name, defaultPort) {
|
|
71
|
+
const pascalName = (0, names_1.kebabToPascal)(name);
|
|
72
|
+
const serviceToken = `${name.toUpperCase()}_SERVICE`;
|
|
73
|
+
return `import { Module } from '@nestjs/common';
|
|
74
|
+
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
75
|
+
import { ${pascalName}GatewayController } from './infra/http/${name}.controller';
|
|
76
|
+
|
|
77
|
+
@Module({
|
|
78
|
+
imports: [
|
|
79
|
+
ClientsModule.register([
|
|
80
|
+
{
|
|
81
|
+
name: '${serviceToken}',
|
|
82
|
+
transport: Transport.TCP,
|
|
83
|
+
options: {
|
|
84
|
+
host: process.env.${name.toUpperCase()}_SERVICE_HOST || 'localhost',
|
|
85
|
+
port: Number(process.env.${name.toUpperCase()}_SERVICE_PORT) || ${defaultPort},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
]),
|
|
89
|
+
],
|
|
90
|
+
controllers: [${pascalName}GatewayController],
|
|
91
|
+
})
|
|
92
|
+
export class ${pascalName}GatewayModule {}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
function nestMsControllerTemplate(name) {
|
|
96
|
+
const pascalName = (0, names_1.kebabToPascal)(name);
|
|
97
|
+
const camelName = (0, names_1.kebabToCamel)(name);
|
|
98
|
+
return `import { Controller } from '@nestjs/common';
|
|
99
|
+
import { MessagePattern } from '@nestjs/microservices';
|
|
100
|
+
import { ${pascalName}Service } from '../../service/${name}.service';
|
|
101
|
+
import { Create${pascalName}Dto, Update${pascalName}Dto, Filter${pascalName}Dto } from './${name}.dto';
|
|
102
|
+
|
|
103
|
+
@Controller()
|
|
104
|
+
export class ${pascalName}MsController {
|
|
105
|
+
constructor(private readonly ${camelName}Service: ${pascalName}Service) {}
|
|
106
|
+
|
|
107
|
+
@MessagePattern({ cmd: 'create_${name}' })
|
|
108
|
+
async create(payload: { data: Create${pascalName}Dto, tenantId: string }) {
|
|
109
|
+
return this.${camelName}Service.create(payload.data);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@MessagePattern({ cmd: 'update_${name}' })
|
|
113
|
+
async update(payload: { id: number, dto: Update${pascalName}Dto, tenantId: string }) {
|
|
114
|
+
return this.${camelName}Service.update(payload.id, payload.dto);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@MessagePattern({ cmd: 'find_one_${name}' })
|
|
118
|
+
async findOne(payload: { id: number, tenantId: string }) {
|
|
119
|
+
return this.${camelName}Service.findOne(payload.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@MessagePattern({ cmd: 'find_many_${name}' })
|
|
123
|
+
async findMany(payload: { filter: Filter${pascalName}Dto, tenantId: string }) {
|
|
124
|
+
return this.${camelName}Service.findMany(payload.filter);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@MessagePattern({ cmd: 'delete_${name}' })
|
|
128
|
+
async delete(payload: { id: number, tenantId: string }) {
|
|
129
|
+
return this.${camelName}Service.delete(payload.id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
function nestMsModuleTemplate(name) {
|
|
135
|
+
const pascalName = (0, names_1.kebabToPascal)(name);
|
|
136
|
+
return `import { Module } from '@nestjs/common';
|
|
137
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
138
|
+
import { ${pascalName}OrmEntity } from './infra/database/${name}.orm-entity';
|
|
139
|
+
import { ${pascalName}RepositoryAdapter } from './infra/database/${name}.repository';
|
|
140
|
+
import { ${pascalName}Service } from './service/${name}.service';
|
|
141
|
+
import { ${pascalName}MsController } from './infra/http/${name}.ms.controller';
|
|
142
|
+
|
|
143
|
+
@Module({
|
|
144
|
+
imports: [TypeOrmModule.forFeature([${pascalName}OrmEntity])],
|
|
145
|
+
controllers: [${pascalName}MsController],
|
|
146
|
+
providers: [
|
|
147
|
+
${pascalName}Service,
|
|
148
|
+
{
|
|
149
|
+
provide: 'I${pascalName}Repository',
|
|
150
|
+
useClass: ${pascalName}RepositoryAdapter,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
exports: [${pascalName}Service],
|
|
154
|
+
})
|
|
155
|
+
export class ${pascalName}Module {}
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { IDevkitEntityConfig, IDevkitEntityProperty } from '@devstroupe/devkit-core';
|
|
2
|
+
export declare function nestMigrationTemplate(entity: IDevkitEntityConfig, entities: IDevkitEntityConfig[], timestamp: number, properties?: IDevkitEntityProperty[]): string;
|
|
3
|
+
export declare function nestDataSourceTemplate(): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nestMigrationTemplate = nestMigrationTemplate;
|
|
4
|
+
exports.nestDataSourceTemplate = nestDataSourceTemplate;
|
|
5
|
+
const names_1 = require("../shared/names");
|
|
6
|
+
const relationships_1 = require("../shared/relationships");
|
|
7
|
+
function columnType(property) {
|
|
8
|
+
if (property.type === 'number' || property.type === 'relationship')
|
|
9
|
+
return 'int';
|
|
10
|
+
if (property.type === 'boolean')
|
|
11
|
+
return 'boolean';
|
|
12
|
+
if (property.type === 'datepicker')
|
|
13
|
+
return 'datetime';
|
|
14
|
+
return 'varchar';
|
|
15
|
+
}
|
|
16
|
+
function columnDefault(property) {
|
|
17
|
+
if (property.default === undefined)
|
|
18
|
+
return '';
|
|
19
|
+
if (typeof property.default === 'boolean')
|
|
20
|
+
return `, default: ${property.default ? "'1'" : "'0'"}`;
|
|
21
|
+
if (typeof property.default === 'number')
|
|
22
|
+
return `, default: '${property.default}'`;
|
|
23
|
+
return `, default: ${JSON.stringify(`'${String(property.default).replace(/'/g, "''")}'`)}`;
|
|
24
|
+
}
|
|
25
|
+
function targetTable(target, entities) {
|
|
26
|
+
const entity = entities.find((candidate) => candidate.name === target);
|
|
27
|
+
return entity?.tableName ?? `${target}s`;
|
|
28
|
+
}
|
|
29
|
+
function nestMigrationTemplate(entity, entities, timestamp, properties = entity.properties) {
|
|
30
|
+
const className = `Create${(0, names_1.kebabToPascal)(entity.name)}${timestamp}`;
|
|
31
|
+
const tableName = entity.tableName ?? `${entity.name}s`;
|
|
32
|
+
const relationships = (0, relationships_1.getRelationships)(properties);
|
|
33
|
+
const scalarColumns = properties
|
|
34
|
+
.filter((property) => property.type !== 'relationship')
|
|
35
|
+
.map((property) => ` { name: '${property.name}', type: '${columnType(property)}', isNullable: ${!property.required}${columnDefault(property)} }`);
|
|
36
|
+
const relationColumns = relationships
|
|
37
|
+
.filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
|
|
38
|
+
.map((relationship) => ` { name: '${relationship.foreignKey}', type: 'int', isNullable: ${!relationship.property.required}, isUnique: ${relationship.kind === 'one-to-one'} }`);
|
|
39
|
+
const foreignKeys = relationships
|
|
40
|
+
.filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
|
|
41
|
+
.map((relationship) => ` new TableForeignKey({
|
|
42
|
+
name: 'FK_${tableName}_${relationship.foreignKey}',
|
|
43
|
+
columnNames: ['${relationship.foreignKey}'],
|
|
44
|
+
referencedTableName: '${targetTable(relationship.target, entities)}',
|
|
45
|
+
referencedColumnNames: ['${relationship.valueField}'],
|
|
46
|
+
onDelete: '${relationship.onDelete}'
|
|
47
|
+
})`)
|
|
48
|
+
.join(',\n');
|
|
49
|
+
const junctions = relationships
|
|
50
|
+
.filter((relationship) => relationship.kind === 'many-to-many' && relationship.owner)
|
|
51
|
+
.map((relationship) => {
|
|
52
|
+
const referencedTable = targetTable(relationship.target, entities);
|
|
53
|
+
const junctionTable = relationship.joinTableName ?? `${entity.name}_${relationship.target}`;
|
|
54
|
+
const sourceColumn = `${entity.name}_id`;
|
|
55
|
+
const targetColumn = `${relationship.target}_id`;
|
|
56
|
+
return {
|
|
57
|
+
tableName: junctionTable,
|
|
58
|
+
up: ` await queryRunner.createTable(new Table({
|
|
59
|
+
name: '${junctionTable}',
|
|
60
|
+
columns: [
|
|
61
|
+
{ name: '${sourceColumn}', type: 'int', isPrimary: true },
|
|
62
|
+
{ name: '${targetColumn}', type: 'int', isPrimary: true }
|
|
63
|
+
],
|
|
64
|
+
foreignKeys: [
|
|
65
|
+
{
|
|
66
|
+
name: 'FK_${junctionTable}_${sourceColumn}',
|
|
67
|
+
columnNames: ['${sourceColumn}'],
|
|
68
|
+
referencedTableName: '${tableName}',
|
|
69
|
+
referencedColumnNames: ['id'],
|
|
70
|
+
onDelete: 'CASCADE'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'FK_${junctionTable}_${targetColumn}',
|
|
74
|
+
columnNames: ['${targetColumn}'],
|
|
75
|
+
referencedTableName: '${referencedTable}',
|
|
76
|
+
referencedColumnNames: ['${relationship.valueField}'],
|
|
77
|
+
onDelete: 'CASCADE'
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}), true);`,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
return `import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
|
84
|
+
|
|
85
|
+
export class ${className} implements MigrationInterface {
|
|
86
|
+
name = '${className}';
|
|
87
|
+
|
|
88
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
89
|
+
await queryRunner.createTable(new Table({
|
|
90
|
+
name: '${tableName}',
|
|
91
|
+
columns: [
|
|
92
|
+
{ name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment' }${scalarColumns.length + relationColumns.length > 0 ? ',\n' : ''}${[...scalarColumns, ...relationColumns].join(',\n')}
|
|
93
|
+
]
|
|
94
|
+
}), true);
|
|
95
|
+
${foreignKeys ? `
|
|
96
|
+
await queryRunner.createForeignKeys('${tableName}', [
|
|
97
|
+
${foreignKeys}
|
|
98
|
+
]);` : ''}
|
|
99
|
+
${junctions.map((junction) => `
|
|
100
|
+
${junction.up}`).join('\n')}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
104
|
+
${junctions.slice().reverse().map((junction) => ` await queryRunner.dropTable('${junction.tableName}', true);`).join('\n')}${junctions.length > 0 ? '\n' : ''} await queryRunner.dropTable('${tableName}', true);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
function nestDataSourceTemplate() {
|
|
110
|
+
return `import 'reflect-metadata';
|
|
111
|
+
import 'dotenv/config';
|
|
112
|
+
import { DataSource } from 'typeorm';
|
|
113
|
+
import * as path from 'path';
|
|
114
|
+
|
|
115
|
+
export default new DataSource({
|
|
116
|
+
type: 'mysql',
|
|
117
|
+
host: process.env.DB_HOST || 'localhost',
|
|
118
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
119
|
+
username: process.env.DB_USER || 'root',
|
|
120
|
+
password: process.env.DB_PASSWORD || 'root',
|
|
121
|
+
database: process.env.DB_NAME || 'devstroupe',
|
|
122
|
+
entities: [path.join(__dirname, '../modules/**/infra/database/*.orm-entity{.ts,.js}')],
|
|
123
|
+
migrations: [path.join(__dirname, './migrations/*{.ts,.js}')],
|
|
124
|
+
synchronize: false
|
|
125
|
+
});
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
9
|
+
const form_template_1 = require("./angular/form.template");
|
|
10
|
+
const list_template_1 = require("./angular/list.template");
|
|
11
|
+
const crud_templates_1 = require("./nest/crud.templates");
|
|
12
|
+
const microservice_templates_1 = require("./nest/microservice.templates");
|
|
13
|
+
const migration_template_1 = require("./nest/migration.template");
|
|
14
|
+
const relationships_1 = require("./shared/relationships");
|
|
15
|
+
const sort_entities_1 = require("../migrations/sort-entities");
|
|
16
|
+
const select_template_1 = require("./ui/components/select.template");
|
|
17
|
+
const entities = [
|
|
18
|
+
{
|
|
19
|
+
name: 'company',
|
|
20
|
+
label: 'Empresa',
|
|
21
|
+
tableName: 'companies',
|
|
22
|
+
properties: [
|
|
23
|
+
{ name: 'name', type: 'string', required: true, label: 'Nome' },
|
|
24
|
+
{
|
|
25
|
+
name: 'cabins',
|
|
26
|
+
type: 'relationship',
|
|
27
|
+
label: 'Cabines',
|
|
28
|
+
relation: {
|
|
29
|
+
target: 'cabin',
|
|
30
|
+
kind: 'one-to-many',
|
|
31
|
+
foreignKey: 'company_id',
|
|
32
|
+
inverseSide: 'company',
|
|
33
|
+
control: 'table',
|
|
34
|
+
columns: ['name', 'code'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'cabin',
|
|
41
|
+
label: 'Cabine',
|
|
42
|
+
tableName: 'cabins',
|
|
43
|
+
properties: [
|
|
44
|
+
{ name: 'name', type: 'string', required: true, label: 'Nome' },
|
|
45
|
+
{ name: 'code', type: 'string', label: 'Código' },
|
|
46
|
+
{
|
|
47
|
+
name: 'company_id',
|
|
48
|
+
type: 'relationship',
|
|
49
|
+
required: true,
|
|
50
|
+
label: 'Empresa',
|
|
51
|
+
relation: {
|
|
52
|
+
target: 'company',
|
|
53
|
+
kind: 'many-to-one',
|
|
54
|
+
relationName: 'company',
|
|
55
|
+
inverseSide: 'cabins',
|
|
56
|
+
control: 'select',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
function assertValidTypeScript(source) {
|
|
63
|
+
const result = typescript_1.default.transpileModule(source, {
|
|
64
|
+
compilerOptions: { target: typescript_1.default.ScriptTarget.ES2022, experimentalDecorators: true },
|
|
65
|
+
reportDiagnostics: true,
|
|
66
|
+
});
|
|
67
|
+
const errors = (result.diagnostics ?? []).filter((diagnostic) => diagnostic.category === typescript_1.default.DiagnosticCategory.Error);
|
|
68
|
+
strict_1.default.deepEqual(errors.map((error) => error.messageText), []);
|
|
69
|
+
}
|
|
70
|
+
(0, node_test_1.default)('relationship config generates select and embedded dependent table', () => {
|
|
71
|
+
(0, relationships_1.validateRelationships)(entities);
|
|
72
|
+
const companyHtml = (0, form_template_1.angularFormHtmlTemplate)('company', entities[0].properties, entities);
|
|
73
|
+
const companyTs = (0, form_template_1.angularFormTsTemplate)('company', entities[0].properties, entities);
|
|
74
|
+
const cabinHtml = (0, form_template_1.angularFormHtmlTemplate)('cabin', entities[1].properties, entities);
|
|
75
|
+
const cabinTs = (0, form_template_1.angularFormTsTemplate)('cabin', entities[1].properties, entities);
|
|
76
|
+
const cabinListTs = (0, list_template_1.angularListTsTemplate)('cabin', entities[1].properties);
|
|
77
|
+
strict_1.default.match(companyHtml, /<devkit-table/);
|
|
78
|
+
strict_1.default.match(companyHtml, /previousCabinsPage/);
|
|
79
|
+
strict_1.default.match(companyHtml, /<app-cabin-form/);
|
|
80
|
+
strict_1.default.match(companyHtml, /\[initialValues\]="cabinsInitialValues"/);
|
|
81
|
+
strict_1.default.match(companyHtml, /\[hiddenFields\]="\['company_id'\]"/);
|
|
82
|
+
strict_1.default.match(companyTs, /filter: \{ .*company_id: this\.itemId \}/);
|
|
83
|
+
strict_1.default.match(cabinHtml, /formControlName="company_id"/);
|
|
84
|
+
strict_1.default.match(cabinHtml, /<devkit-select/);
|
|
85
|
+
strict_1.default.match(cabinListTs, /options: this\.companyIdLookup\.options/);
|
|
86
|
+
strict_1.default.match(cabinListTs, /pageSize: 20/);
|
|
87
|
+
strict_1.default.match(cabinListTs, /companyIdLookup\.setSearch\(search\)/);
|
|
88
|
+
strict_1.default.match(cabinTs, /new RelationshipLookup<any>/);
|
|
89
|
+
assertValidTypeScript(companyTs);
|
|
90
|
+
assertValidTypeScript(cabinTs);
|
|
91
|
+
assertValidTypeScript(cabinListTs);
|
|
92
|
+
});
|
|
93
|
+
(0, node_test_1.default)('relationship select exposes debounced remote search and incremental loading', () => {
|
|
94
|
+
const select = (0, select_template_1.devkitSelectTemplate)();
|
|
95
|
+
strict_1.default.match(select.ts, /searchChange = new EventEmitter<string>/);
|
|
96
|
+
strict_1.default.match(select.ts, /setTimeout\(\(\) => this\.searchChange\.emit/);
|
|
97
|
+
strict_1.default.match(select.html, /Carregar mais/);
|
|
98
|
+
strict_1.default.match(select.html, /loadMore\.emit\(\)/);
|
|
99
|
+
assertValidTypeScript(select.ts);
|
|
100
|
+
});
|
|
101
|
+
(0, node_test_1.default)('relationship config generates TypeORM joins and scoped repository filters', () => {
|
|
102
|
+
const companyOrm = (0, crud_templates_1.nestOrmEntityTemplate)('company', entities[0].properties, 'companies', false);
|
|
103
|
+
const cabinOrm = (0, crud_templates_1.nestOrmEntityTemplate)('cabin', entities[1].properties, 'cabins', false);
|
|
104
|
+
const companyRepository = (0, crud_templates_1.nestRepositoryAdapterTemplate)('company', entities[0].properties, false);
|
|
105
|
+
const cabinRepository = (0, crud_templates_1.nestRepositoryAdapterTemplate)('cabin', entities[1].properties, false);
|
|
106
|
+
strict_1.default.match(companyOrm, /@OneToMany\(\(\) => CabinOrmEntity, target => target\.company\)/);
|
|
107
|
+
strict_1.default.match(cabinOrm, /@ManyToOne\(\(\) => CompanyOrmEntity, target => target\.cabins/);
|
|
108
|
+
strict_1.default.match(cabinOrm, /@JoinColumn\(\{ name: 'company_id', referencedColumnName: 'id' \}\)/);
|
|
109
|
+
strict_1.default.match(cabinRepository, /qb\.leftJoinAndSelect\(`\$\{alias\}\.company`, 'company'\)/);
|
|
110
|
+
strict_1.default.match(cabinRepository, /allowedFilters: \['name', 'code', 'company_id'\]/);
|
|
111
|
+
strict_1.default.doesNotMatch(companyRepository, /leftJoinAndSelect/);
|
|
112
|
+
assertValidTypeScript(companyOrm);
|
|
113
|
+
assertValidTypeScript(cabinOrm);
|
|
114
|
+
assertValidTypeScript(companyRepository);
|
|
115
|
+
assertValidTypeScript(cabinRepository);
|
|
116
|
+
});
|
|
117
|
+
(0, node_test_1.default)('many-to-many generates a multiple selector and owning join table', () => {
|
|
118
|
+
const manyToManyEntities = [
|
|
119
|
+
{ name: 'role', properties: [{ name: 'name', type: 'string', label: 'Nome' }] },
|
|
120
|
+
{
|
|
121
|
+
name: 'user',
|
|
122
|
+
properties: [{
|
|
123
|
+
name: 'roles',
|
|
124
|
+
type: 'relationship',
|
|
125
|
+
label: 'Perfis',
|
|
126
|
+
relation: { target: 'role', kind: 'many-to-many', control: 'multi-select' },
|
|
127
|
+
}],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
(0, relationships_1.validateRelationships)(manyToManyEntities);
|
|
131
|
+
const html = (0, form_template_1.angularFormHtmlTemplate)('user', manyToManyEntities[1].properties, manyToManyEntities);
|
|
132
|
+
const orm = (0, crud_templates_1.nestOrmEntityTemplate)('user', manyToManyEntities[1].properties, 'users', false);
|
|
133
|
+
const repository = (0, crud_templates_1.nestRepositoryAdapterTemplate)('user', manyToManyEntities[1].properties, false);
|
|
134
|
+
const migration = (0, migration_template_1.nestMigrationTemplate)({ ...manyToManyEntities[1], tableName: 'users' }, manyToManyEntities, 2000);
|
|
135
|
+
strict_1.default.match(html, /\[multiple\]="true"/);
|
|
136
|
+
strict_1.default.match(orm, /@ManyToMany\(\(\) => RoleOrmEntity\)/);
|
|
137
|
+
strict_1.default.match(orm, /@JoinTable\(\{/);
|
|
138
|
+
strict_1.default.match(orm, /name: 'user_role'/);
|
|
139
|
+
strict_1.default.match(repository, /orm\.roles = domain\.roles!\.map/);
|
|
140
|
+
strict_1.default.match(repository, /ormRepository\.preload/);
|
|
141
|
+
strict_1.default.match(migration, /name: 'user_role'/);
|
|
142
|
+
strict_1.default.match(migration, /name: 'user_id', type: 'int', isPrimary: true/);
|
|
143
|
+
assertValidTypeScript(migration);
|
|
144
|
+
});
|
|
145
|
+
(0, node_test_1.default)('relationship migrations create foreign keys and owning junction tables', () => {
|
|
146
|
+
const companyMigration = (0, migration_template_1.nestMigrationTemplate)(entities[0], entities, 1000);
|
|
147
|
+
const cabinMigration = (0, migration_template_1.nestMigrationTemplate)(entities[1], entities, 1001);
|
|
148
|
+
const dataSource = (0, migration_template_1.nestDataSourceTemplate)();
|
|
149
|
+
strict_1.default.match(companyMigration, /name: 'companies'/);
|
|
150
|
+
strict_1.default.doesNotMatch(companyMigration, /FK_companies_cabins/);
|
|
151
|
+
strict_1.default.match(cabinMigration, /name: 'company_id', type: 'int'/);
|
|
152
|
+
strict_1.default.match(cabinMigration, /referencedTableName: 'companies'/);
|
|
153
|
+
strict_1.default.match(cabinMigration, /onDelete: 'CASCADE'/);
|
|
154
|
+
strict_1.default.match(dataSource, /migrations: \[path\.join/);
|
|
155
|
+
strict_1.default.match(dataSource, /synchronize: false/);
|
|
156
|
+
assertValidTypeScript(companyMigration);
|
|
157
|
+
assertValidTypeScript(cabinMigration);
|
|
158
|
+
assertValidTypeScript(dataSource);
|
|
159
|
+
const ordered = (0, sort_entities_1.sortEntitiesForMigrations)([entities[1], entities[0]]);
|
|
160
|
+
strict_1.default.deepEqual(ordered.map((entity) => entity.name), ['company', 'cabin']);
|
|
161
|
+
});
|
|
162
|
+
(0, node_test_1.default)('invalid relationship targets fail before files are generated', () => {
|
|
163
|
+
strict_1.default.throws(() => (0, relationships_1.validateRelationships)([{
|
|
164
|
+
name: 'cabin',
|
|
165
|
+
properties: [{
|
|
166
|
+
name: 'company_id',
|
|
167
|
+
type: 'relationship',
|
|
168
|
+
label: 'Empresa',
|
|
169
|
+
relation: { target: 'missing-company' },
|
|
170
|
+
}],
|
|
171
|
+
}]), /targets unknown entity/);
|
|
172
|
+
});
|
|
173
|
+
(0, node_test_1.default)('kebab-case entities generate valid controller identifiers', () => {
|
|
174
|
+
const controller = (0, crud_templates_1.nestControllerTemplate)('item-pedido');
|
|
175
|
+
const msController = (0, microservice_templates_1.nestMsControllerTemplate)('item-pedido');
|
|
176
|
+
strict_1.default.match(controller, /itemPedidoService: ItemPedidoService/);
|
|
177
|
+
strict_1.default.doesNotMatch(controller, /item-pedidoService/);
|
|
178
|
+
strict_1.default.match(msController, /this\.itemPedidoService\.findMany/);
|
|
179
|
+
assertValidTypeScript(controller);
|
|
180
|
+
assertValidTypeScript(msController);
|
|
181
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.capitalize = capitalize;
|
|
4
|
+
exports.kebabToCamel = kebabToCamel;
|
|
5
|
+
exports.kebabToPascal = kebabToPascal;
|
|
6
|
+
function capitalize(s) {
|
|
7
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
8
|
+
}
|
|
9
|
+
function kebabToCamel(s) {
|
|
10
|
+
return s.replace(/[-_](.)/g, (_, character) => character.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
function kebabToPascal(s) {
|
|
13
|
+
return capitalize(kebabToCamel(s));
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DevkitRelationshipControl, DevkitRelationshipKind, IDevkitEntityConfig, IDevkitEntityProperty, IDevkitRelationshipColumn } from '@devstroupe/devkit-core';
|
|
2
|
+
export interface NormalizedRelationship {
|
|
3
|
+
property: IDevkitEntityProperty;
|
|
4
|
+
target: string;
|
|
5
|
+
kind: DevkitRelationshipKind;
|
|
6
|
+
control: DevkitRelationshipControl;
|
|
7
|
+
displayField: string;
|
|
8
|
+
valueField: string;
|
|
9
|
+
relationName: string;
|
|
10
|
+
foreignKey?: string;
|
|
11
|
+
inverseSide?: string;
|
|
12
|
+
owner: boolean;
|
|
13
|
+
embedded: boolean;
|
|
14
|
+
onDelete: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
|
|
15
|
+
columns: IDevkitRelationshipColumn[];
|
|
16
|
+
filters: Record<string, unknown>;
|
|
17
|
+
lookupMode: 'remote' | 'all';
|
|
18
|
+
pageSize: number;
|
|
19
|
+
minSearchLength: number;
|
|
20
|
+
joinTableName?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function isCollectionRelationship(kind: DevkitRelationshipKind): boolean;
|
|
23
|
+
export declare function isSingularRelationship(kind: DevkitRelationshipKind): boolean;
|
|
24
|
+
export declare function normalizeRelationship(property: IDevkitEntityProperty): NormalizedRelationship;
|
|
25
|
+
export declare function getRelationships(properties: IDevkitEntityProperty[]): NormalizedRelationship[];
|
|
26
|
+
export declare function getTargetEntity(relationship: NormalizedRelationship, entities: IDevkitEntityConfig[]): IDevkitEntityConfig | undefined;
|
|
27
|
+
export declare function validateRelationships(entities: IDevkitEntityConfig[]): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isCollectionRelationship = isCollectionRelationship;
|
|
4
|
+
exports.isSingularRelationship = isSingularRelationship;
|
|
5
|
+
exports.normalizeRelationship = normalizeRelationship;
|
|
6
|
+
exports.getRelationships = getRelationships;
|
|
7
|
+
exports.getTargetEntity = getTargetEntity;
|
|
8
|
+
exports.validateRelationships = validateRelationships;
|
|
9
|
+
function isCollectionRelationship(kind) {
|
|
10
|
+
return kind === 'one-to-many' || kind === 'many-to-many';
|
|
11
|
+
}
|
|
12
|
+
function isSingularRelationship(kind) {
|
|
13
|
+
return !isCollectionRelationship(kind);
|
|
14
|
+
}
|
|
15
|
+
function normalizeRelationship(property) {
|
|
16
|
+
if (property.type !== 'relationship' || !property.relation?.target) {
|
|
17
|
+
throw new Error(`Relationship property "${property.name}" must define relation.target.`);
|
|
18
|
+
}
|
|
19
|
+
const config = property.relation;
|
|
20
|
+
const kind = config.kind ?? 'many-to-one';
|
|
21
|
+
const collection = isCollectionRelationship(kind);
|
|
22
|
+
const relationName = config.relationName
|
|
23
|
+
?? (collection ? property.name : config.target);
|
|
24
|
+
const control = config.control
|
|
25
|
+
?? (kind === 'one-to-many' ? 'table' : kind === 'many-to-many' ? 'multi-select' : 'select');
|
|
26
|
+
const foreignKey = config.foreignKey
|
|
27
|
+
?? (isSingularRelationship(kind) ? property.name : undefined);
|
|
28
|
+
return {
|
|
29
|
+
property,
|
|
30
|
+
target: config.target,
|
|
31
|
+
kind,
|
|
32
|
+
control,
|
|
33
|
+
displayField: config.displayField ?? 'name',
|
|
34
|
+
valueField: config.valueField ?? 'id',
|
|
35
|
+
relationName,
|
|
36
|
+
foreignKey,
|
|
37
|
+
inverseSide: config.inverseSide,
|
|
38
|
+
owner: config.owner ?? true,
|
|
39
|
+
embedded: config.embedded ?? kind === 'one-to-many',
|
|
40
|
+
onDelete: config.onDelete ?? (property.required ? 'CASCADE' : 'SET NULL'),
|
|
41
|
+
columns: (config.columns ?? []).map((column) => typeof column === 'string' ? { key: column } : column),
|
|
42
|
+
filters: config.filters ?? {},
|
|
43
|
+
lookupMode: config.lookup?.mode ?? 'remote',
|
|
44
|
+
pageSize: config.lookup?.pageSize ?? 20,
|
|
45
|
+
minSearchLength: config.lookup?.minSearchLength ?? 0,
|
|
46
|
+
joinTableName: config.joinTableName,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function getRelationships(properties) {
|
|
50
|
+
return properties
|
|
51
|
+
.filter((property) => property.type === 'relationship')
|
|
52
|
+
.map(normalizeRelationship);
|
|
53
|
+
}
|
|
54
|
+
function getTargetEntity(relationship, entities) {
|
|
55
|
+
return entities.find((entity) => entity.name === relationship.target);
|
|
56
|
+
}
|
|
57
|
+
function validateRelationships(entities) {
|
|
58
|
+
const entityNames = new Set(entities.map((entity) => entity.name));
|
|
59
|
+
for (const entity of entities) {
|
|
60
|
+
for (const property of entity.properties) {
|
|
61
|
+
if (property.type !== 'relationship')
|
|
62
|
+
continue;
|
|
63
|
+
const relationship = normalizeRelationship(property);
|
|
64
|
+
if (!entityNames.has(relationship.target)) {
|
|
65
|
+
throw new Error(`Entity "${entity.name}" relationship "${property.name}" targets unknown entity "${relationship.target}".`);
|
|
66
|
+
}
|
|
67
|
+
if (isSingularRelationship(relationship.kind) && relationship.foreignKey === relationship.relationName) {
|
|
68
|
+
throw new Error(`Entity "${entity.name}" relationship "${property.name}" must use different foreignKey and relationName values.`);
|
|
69
|
+
}
|
|
70
|
+
if (relationship.kind === 'one-to-one' && !relationship.owner) {
|
|
71
|
+
throw new Error(`Entity "${entity.name}" inverse one-to-one relationship "${property.name}" is not an owning form field. Declare it on the owning entity or model it as an embedded dependent.`);
|
|
72
|
+
}
|
|
73
|
+
if (relationship.kind === 'many-to-many' && !relationship.owner) {
|
|
74
|
+
throw new Error(`Entity "${entity.name}" many-to-many relationship "${property.name}" must be generated on the owning side.`);
|
|
75
|
+
}
|
|
76
|
+
if (relationship.kind === 'one-to-many' && !relationship.foreignKey) {
|
|
77
|
+
throw new Error(`Entity "${entity.name}" one-to-many relationship "${property.name}" must define relation.foreignKey.`);
|
|
78
|
+
}
|
|
79
|
+
if (relationship.kind === 'one-to-many') {
|
|
80
|
+
const target = entities.find((candidate) => candidate.name === relationship.target);
|
|
81
|
+
const targetHasForeignKey = target?.properties.some((candidate) => candidate.name === relationship.foreignKey
|
|
82
|
+
|| (candidate.type === 'relationship'
|
|
83
|
+
&& normalizeRelationship(candidate).foreignKey === relationship.foreignKey));
|
|
84
|
+
if (!targetHasForeignKey) {
|
|
85
|
+
throw new Error(`Entity "${entity.name}" relationship "${property.name}" expects foreign key "${relationship.foreignKey}" on "${relationship.target}".`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (isSingularRelationship(relationship.kind) && !['select'].includes(relationship.control)) {
|
|
89
|
+
throw new Error(`Entity "${entity.name}" singular relationship "${property.name}" must use control "select".`);
|
|
90
|
+
}
|
|
91
|
+
if (relationship.kind === 'many-to-many' && relationship.control !== 'multi-select') {
|
|
92
|
+
throw new Error(`Entity "${entity.name}" many-to-many relationship "${property.name}" must use control "multi-select".`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|