@devstroupe/devkit-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boilerplates/angular-template/node_modules/.bin/acorn +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/eslint +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/js-yaml +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/ng +2 -2
- package/dist/boilerplates/angular-template/node_modules/.bin/nx +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/nx-cloud +21 -0
- package/dist/boilerplates/angular-template/node_modules/.bin/sass +4 -4
- package/dist/boilerplates/angular-template/node_modules/.bin/vite +4 -4
- package/dist/boilerplates/angular-template/node_modules/.bin/webpack +21 -0
- package/dist/boilerplates/angular-template/package.json +1 -1
- package/dist/boilerplates/angular-template/src/app/app.config.ts +3 -0
- package/dist/boilerplates/angular-template/src/app/app.routes.ts +7 -0
- package/dist/boilerplates/angular-template/src/app/modules/role/role-permissions.component.css +38 -0
- package/dist/boilerplates/angular-template/src/app/modules/role/role-permissions.component.html +175 -0
- package/dist/boilerplates/angular-template/src/app/modules/role/role-permissions.component.ts +134 -0
- package/dist/boilerplates/angular-template/src/app/services/role.service.ts +12 -0
- package/dist/boilerplates/angular-template/src/app/services/user.service.ts +12 -0
- package/dist/boilerplates/angular-template/src/app/shared/components/unauthorized/unauthorized.component.css +4 -0
- package/dist/boilerplates/angular-template/src/app/shared/components/unauthorized/unauthorized.component.html +18 -0
- package/dist/boilerplates/angular-template/src/app/shared/components/unauthorized/unauthorized.component.ts +18 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-cwd +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-esm +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-script +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-transpile-only +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-script +21 -0
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm +2 -2
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-commonjs +2 -2
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-esm +2 -2
- package/dist/boilerplates/nest-template/package.json +1 -1
- package/dist/boilerplates/nest-template/src/app.module.ts +25 -0
- package/dist/boilerplates/nest-template/src/database/migrations/1600000000000-create-roles.ts +20 -0
- package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +13 -2
- package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +16 -0
- package/dist/boilerplates/nest-template/src/modules/auth/auth.module.ts +2 -0
- package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +54 -8
- package/dist/boilerplates/nest-template/src/modules/role/domain/role.repository.ts +10 -0
- package/dist/boilerplates/nest-template/src/modules/role/domain/role.ts +5 -0
- package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.orm-entity.ts +13 -0
- package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.repository.adapter.ts +54 -0
- package/dist/boilerplates/nest-template/src/modules/role/infra/http/role.controller.ts +51 -0
- package/dist/boilerplates/nest-template/src/modules/role/infra/http/role.dto.ts +17 -0
- package/dist/boilerplates/nest-template/src/modules/role/role.module.ts +20 -0
- package/dist/boilerplates/nest-template/src/modules/role/service/role.service.ts +39 -0
- package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage-download.controller.ts +38 -0
- package/dist/boilerplates/nest-template/src/modules/storage/storage.module.ts +7 -0
- package/dist/boilerplates/nest-template/src/modules/user/database-seed.service.ts +59 -18
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +1 -0
- package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +4 -1
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +8 -3
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +6 -0
- package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +56 -0
- package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +4 -0
- package/dist/boilerplates/nest-template/src/modules/user/user.module.ts +7 -1
- package/dist/index.js +24 -3
- package/dist/templates/angular/form.template.js +85 -9
- package/dist/templates/angular/list.template.js +27 -5
- package/dist/templates/nest/crud.templates.d.ts +16 -2
- package/dist/templates/nest/crud.templates.js +175 -13
- package/dist/templates/nest/migration.template.js +82 -6
- package/dist/templates/shared/relationships.js +7 -4
- package/dist/templates/ui/components/index.d.ts +1 -0
- package/dist/templates/ui/components/index.js +1 -0
- package/dist/templates/ui/components/input.template.js +17 -9
- package/dist/templates/ui/components/textarea.template.d.ts +4 -0
- package/dist/templates/ui/components/textarea.template.js +76 -0
- package/dist/templates/ui/shared/profile-component.template.js +3 -3
- package/package.json +3 -3
|
@@ -7,6 +7,18 @@ const relationships_1 = require("../shared/relationships");
|
|
|
7
7
|
function angularListHtmlTemplate(name, properties, listType = 'table', formType = 'page', filterType = 'popover') {
|
|
8
8
|
const pascalName = (0, names_1.kebabToPascal)(name);
|
|
9
9
|
let listComponentHtml = '';
|
|
10
|
+
const currencyProperties = properties.filter((p) => p.type === 'currency');
|
|
11
|
+
const hasCurrency = currencyProperties.length > 0;
|
|
12
|
+
const customTemplatesAttr = hasCurrency
|
|
13
|
+
? `\n [customTemplates]="{ ${currencyProperties.map((p) => `${p.name}: ${p.name}Temp`).join(', ')} }"`
|
|
14
|
+
: '';
|
|
15
|
+
const templatesHtml = currencyProperties
|
|
16
|
+
.map((p) => `
|
|
17
|
+
|
|
18
|
+
<ng-template #${p.name}Temp let-row>
|
|
19
|
+
{{ row.${p.name} | currency:'BRL':'symbol':'1.2-2' }}
|
|
20
|
+
</ng-template>`)
|
|
21
|
+
.join('');
|
|
10
22
|
if (listType === 'card') {
|
|
11
23
|
listComponentHtml = ` <!-- Lista em Cards Dinâmica -->
|
|
12
24
|
<devkit-card-list
|
|
@@ -32,8 +44,8 @@ function angularListHtmlTemplate(name, properties, listType = 'table', formType
|
|
|
32
44
|
[currentDirection]="$any(filter).direction || 'ASC'"
|
|
33
45
|
[striped]="true"
|
|
34
46
|
(sortChange)="onSort($event)"
|
|
35
|
-
(rowClick)="editItem($event.id)"
|
|
36
|
-
></devkit-table
|
|
47
|
+
(rowClick)="editItem($event.id)"${customTemplatesAttr}
|
|
48
|
+
></devkit-table>${templatesHtml}`;
|
|
37
49
|
}
|
|
38
50
|
const routerOutletHtml = formType === 'dialog' ? '\n <!-- Outlet para carregar o Dialog Handler -->\n <router-outlet></router-outlet>' : '';
|
|
39
51
|
return `<div class="flex flex-col space-y-4 text-foreground">
|
|
@@ -57,11 +69,16 @@ function angularListTsTemplate(name, properties, listType = 'table', formType =
|
|
|
57
69
|
const filterConfig = properties
|
|
58
70
|
.filter((p) => p.type !== 'relationship')
|
|
59
71
|
.map((p) => {
|
|
72
|
+
const type = p.type;
|
|
60
73
|
let typeStr = 'text';
|
|
61
|
-
if (
|
|
74
|
+
if (type === 'boolean')
|
|
62
75
|
typeStr = 'boolean';
|
|
63
|
-
if (
|
|
76
|
+
if (type === 'datepicker' || type === 'date' || type === 'datetime')
|
|
64
77
|
typeStr = 'datepicker';
|
|
78
|
+
if (type === 'enum' && p.enumOptions) {
|
|
79
|
+
const options = p.enumOptions.map((o) => ({ label: o, value: o }));
|
|
80
|
+
return ` { key: '${p.name}', label: '${p.label}', type: 'select', options: ${JSON.stringify(options)} }`;
|
|
81
|
+
}
|
|
65
82
|
return ` { key: '${p.name}', label: '${p.label}', type: '${typeStr}' }`;
|
|
66
83
|
})
|
|
67
84
|
.concat(singularRelationships.map((relationship) => {
|
|
@@ -149,7 +166,7 @@ function angularListTsTemplate(name, properties, listType = 'table', formType =
|
|
|
149
166
|
}).join('\n');
|
|
150
167
|
return `import { ChangeDetectorRef, Component, OnInit, OnDestroy, inject } from '@angular/core';
|
|
151
168
|
import { CommonModule } from '@angular/common';
|
|
152
|
-
import { BaseListPage, RelationshipLookup } from '@devstroupe/devkit-angular';
|
|
169
|
+
import { BaseListPage, RelationshipLookup, RealtimeService } from '@devstroupe/devkit-angular';
|
|
153
170
|
import { ITableColumn } from '@devstroupe/ui/table/table.component';
|
|
154
171
|
${importComponent}
|
|
155
172
|
import { DevkitFilterComponent, FilterItemConfig } from '@devstroupe/ui/filter/filter.component';
|
|
@@ -157,6 +174,7 @@ import { DevkitButtonComponent } from '@devstroupe/ui/button/button.component';
|
|
|
157
174
|
${routeImports}
|
|
158
175
|
${relationshipServiceImports}
|
|
159
176
|
import { ${pascalName}Service } from '../../../services/${name}.service';
|
|
177
|
+
import { takeUntil } from 'rxjs/operators';
|
|
160
178
|
|
|
161
179
|
@Component({
|
|
162
180
|
selector: 'app-${name}-list',
|
|
@@ -170,6 +188,7 @@ export class ${pascalName}ListComponent extends BaseListPage<any> implements OnI
|
|
|
170
188
|
private _router = inject(Router);
|
|
171
189
|
private _route = inject(ActivatedRoute);
|
|
172
190
|
private _changeDetector = inject(ChangeDetectorRef);
|
|
191
|
+
private _realtime = inject(RealtimeService);
|
|
173
192
|
${relationshipInjections}
|
|
174
193
|
|
|
175
194
|
${relationshipState}
|
|
@@ -177,6 +196,9 @@ ${relationshipState}
|
|
|
177
196
|
ngOnInit(): void {
|
|
178
197
|
this.initBaseListPage(this._router, this._route, this._changeDetector);
|
|
179
198
|
${relationshipLoadCalls}
|
|
199
|
+
this._realtime.onEntityChanged('${name}')
|
|
200
|
+
.pipe(takeUntil(this.unsubscribeAll$))
|
|
201
|
+
.subscribe(() => this.loadItems());
|
|
180
202
|
}
|
|
181
203
|
|
|
182
204
|
ngOnDestroy(): void {
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { IDevkitEntityProperty } from '@devstroupe/devkit-core';
|
|
2
2
|
export declare function nestDomainEntityTemplate(name: string, properties: IDevkitEntityProperty[], isTenantScoped: boolean): string;
|
|
3
3
|
export declare function nestRepositoryPortTemplate(name: string): string;
|
|
4
|
-
export declare function nestOrmEntityTemplate(name: string, properties: IDevkitEntityProperty[], tableName: string, isTenantScoped: boolean
|
|
4
|
+
export declare function nestOrmEntityTemplate(name: string, properties: IDevkitEntityProperty[], tableName: string, isTenantScoped: boolean, indexes?: Array<{
|
|
5
|
+
columns: string[];
|
|
6
|
+
unique?: boolean;
|
|
7
|
+
name?: string;
|
|
8
|
+
}>, persistence?: {
|
|
9
|
+
deletion?: 'soft-delete' | 'hard-delete';
|
|
10
|
+
mode?: 'append-only' | 'standard';
|
|
11
|
+
}, automaticFields?: {
|
|
12
|
+
timestamps?: boolean;
|
|
13
|
+
blamers?: boolean;
|
|
14
|
+
optimisticLocking?: boolean;
|
|
15
|
+
}): string;
|
|
5
16
|
export declare function nestRepositoryAdapterTemplate(name: string, properties: IDevkitEntityProperty[], isTenantScoped: boolean): string;
|
|
6
17
|
export declare function nestDtoTemplate(name: string, properties: IDevkitEntityProperty[]): string;
|
|
7
18
|
export declare function nestServiceTemplate(name: string): string;
|
|
8
|
-
export declare function nestControllerTemplate(name: string
|
|
19
|
+
export declare function nestControllerTemplate(name: string, persistence?: {
|
|
20
|
+
deletion?: 'soft-delete' | 'hard-delete';
|
|
21
|
+
mode?: 'append-only' | 'standard';
|
|
22
|
+
}): string;
|
|
9
23
|
export declare function nestModuleTemplate(name: string): string;
|
|
@@ -24,7 +24,16 @@ function nestDomainEntityTemplate(name, properties, isTenantScoped) {
|
|
|
24
24
|
}
|
|
25
25
|
return ` ${relation.foreignKey}!: number;\n ${relation.relationName}?: any;`;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
let typeStr = 'string';
|
|
28
|
+
if (p.type === 'number' || p.type === 'decimal' || p.type === 'currency') {
|
|
29
|
+
typeStr = 'number';
|
|
30
|
+
}
|
|
31
|
+
else if (p.type === 'boolean') {
|
|
32
|
+
typeStr = 'boolean';
|
|
33
|
+
}
|
|
34
|
+
else if (p.type === 'enum' && p.enumOptions) {
|
|
35
|
+
typeStr = p.enumOptions.map(o => `'${o}'`).join(' | ');
|
|
36
|
+
}
|
|
28
37
|
return ` ${p.name}!: ${typeStr};`;
|
|
29
38
|
})
|
|
30
39
|
.join('\n');
|
|
@@ -49,7 +58,7 @@ export interface I${pascalName}Repository {
|
|
|
49
58
|
}
|
|
50
59
|
`;
|
|
51
60
|
}
|
|
52
|
-
function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped) {
|
|
61
|
+
function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped, indexes, persistence, automaticFields) {
|
|
53
62
|
const pascalName = (0, names_1.kebabToPascal)(name);
|
|
54
63
|
const relationships = (0, relationships_1.getRelationships)(properties);
|
|
55
64
|
const typeormImports = ['Entity', 'PrimaryGeneratedColumn', 'Column'];
|
|
@@ -65,6 +74,14 @@ function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped) {
|
|
|
65
74
|
typeormImports.push('JoinColumn');
|
|
66
75
|
if (relationships.some((relation) => relation.kind === 'many-to-many' && relation.owner))
|
|
67
76
|
typeormImports.push('JoinTable');
|
|
77
|
+
if (indexes && indexes.length > 0)
|
|
78
|
+
typeormImports.push('Index');
|
|
79
|
+
if (persistence?.deletion === 'soft-delete')
|
|
80
|
+
typeormImports.push('DeleteDateColumn');
|
|
81
|
+
if (automaticFields?.timestamps)
|
|
82
|
+
typeormImports.push('CreateDateColumn', 'UpdateDateColumn');
|
|
83
|
+
if (automaticFields?.optimisticLocking)
|
|
84
|
+
typeormImports.push('VersionColumn');
|
|
68
85
|
const relImports = [...new Set(relationships.map((relation) => relation.target))]
|
|
69
86
|
.map((targetKebab) => {
|
|
70
87
|
const targetPascal = (0, names_1.kebabToPascal)(targetKebab);
|
|
@@ -98,21 +115,80 @@ function nestOrmEntityTemplate(name, properties, tableName, isTenantScoped) {
|
|
|
98
115
|
const joinColumn = relation.owner ? `\n @JoinColumn({ name: '${relation.foreignKey}', referencedColumnName: '${relation.valueField}' })` : '';
|
|
99
116
|
return ` ${fkDecorator}\n ${relation.foreignKey}!: number;\n\n @${decorator}(() => ${targetPascal}OrmEntity${relation.inverseSide ? inverse : ''}, ${options})${joinColumn}\n ${relation.relationName}?: ${targetPascal}OrmEntity;`;
|
|
100
117
|
}
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
let decorator = p.required ? '@Column()' : '@Column({ nullable: true })';
|
|
119
|
+
let typeStr = 'string';
|
|
120
|
+
const type = p.type;
|
|
121
|
+
if (type === 'number' || type === 'decimal' || type === 'currency' || type === 'integer') {
|
|
122
|
+
typeStr = 'number';
|
|
123
|
+
if (type === 'decimal' || type === 'currency') {
|
|
124
|
+
const precision = p.precision !== undefined ? p.precision : 12;
|
|
125
|
+
const scale = p.scale !== undefined ? p.scale : 2;
|
|
126
|
+
decorator = `@Column({ type: 'decimal', precision: ${precision}, scale: ${scale}${p.required ? '' : ', nullable: true'} })`;
|
|
127
|
+
}
|
|
128
|
+
else if (type === 'integer') {
|
|
129
|
+
decorator = p.required ? `@Column({ type: 'int' })` : `@Column({ type: 'int', nullable: true })`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (type === 'bigint') {
|
|
133
|
+
typeStr = 'string';
|
|
134
|
+
decorator = p.required ? `@Column({ type: 'bigint' })` : `@Column({ type: 'bigint', nullable: true })`;
|
|
135
|
+
}
|
|
136
|
+
else if (type === 'boolean') {
|
|
137
|
+
typeStr = 'boolean';
|
|
138
|
+
}
|
|
139
|
+
else if (type === 'datepicker' || type === 'datetime') {
|
|
140
|
+
typeStr = 'Date';
|
|
141
|
+
decorator = p.required ? `@Column({ type: 'datetime' })` : `@Column({ type: 'datetime', nullable: true })`;
|
|
142
|
+
}
|
|
143
|
+
else if (type === 'date') {
|
|
144
|
+
typeStr = 'Date';
|
|
145
|
+
decorator = p.required ? `@Column({ type: 'date' })` : `@Column({ type: 'date', nullable: true })`;
|
|
146
|
+
}
|
|
147
|
+
else if (type === 'text') {
|
|
148
|
+
decorator = `@Column({ type: 'text'${p.required ? '' : ', nullable: true'} })`;
|
|
149
|
+
}
|
|
150
|
+
else if (type === 'json') {
|
|
151
|
+
typeStr = 'any';
|
|
152
|
+
decorator = p.required ? `@Column({ type: 'json' })` : `@Column({ type: 'json', nullable: true })`;
|
|
153
|
+
}
|
|
154
|
+
else if (type === 'enum' && p.enumOptions) {
|
|
155
|
+
typeStr = p.enumOptions.map(o => `'${o}'`).join(' | ');
|
|
156
|
+
decorator = `@Column({ type: 'enum', enum: ${JSON.stringify(p.enumOptions)}${p.required ? '' : ', nullable: true'} })`;
|
|
157
|
+
}
|
|
103
158
|
return ` ${decorator}\n ${p.name}!: ${typeStr};`;
|
|
104
159
|
})
|
|
105
160
|
.join('\n\n');
|
|
106
161
|
const tenantField = isTenantScoped ? '\n\n @Column()\n tenantId!: string;' : '';
|
|
107
162
|
const relImportsStr = relImports ? `\n${relImports}` : '';
|
|
163
|
+
const isSoftDelete = persistence?.deletion === 'soft-delete';
|
|
164
|
+
const softDeleteField = isSoftDelete ? '\n\n @DeleteDateColumn({ name: \'deleted_at\', nullable: true })\n deletedAt?: Date;' : '';
|
|
165
|
+
let autoFieldsStr = '';
|
|
166
|
+
if (automaticFields?.timestamps) {
|
|
167
|
+
autoFieldsStr += '\n\n @CreateDateColumn({ name: \'created_at\' })\n createdAt!: Date;\n\n @UpdateDateColumn({ name: \'updated_at\' })\n updatedAt!: Date;';
|
|
168
|
+
}
|
|
169
|
+
if (automaticFields?.blamers) {
|
|
170
|
+
autoFieldsStr += '\n\n @Column({ name: \'created_by_user_id\', nullable: true })\n createdByUserId?: number;\n\n @Column({ name: \'updated_by_user_id\', nullable: true })\n updatedByUserId?: number;';
|
|
171
|
+
}
|
|
172
|
+
if (automaticFields?.optimisticLocking) {
|
|
173
|
+
autoFieldsStr += '\n\n @VersionColumn({ default: 1 })\n version!: number;';
|
|
174
|
+
}
|
|
175
|
+
const indexDecorators = indexes && indexes.length > 0
|
|
176
|
+
? indexes.map(idx => {
|
|
177
|
+
const nameOption = idx.name ? `name: '${idx.name}'` : '';
|
|
178
|
+
const uniqueOption = idx.unique ? `unique: true` : '';
|
|
179
|
+
const options = [nameOption, uniqueOption].filter(Boolean).join(', ');
|
|
180
|
+
const optionsStr = options ? `, { ${options} }` : '';
|
|
181
|
+
return `@Index(${JSON.stringify(idx.columns)}${optionsStr})`;
|
|
182
|
+
}).join('\n') + '\n'
|
|
183
|
+
: '';
|
|
108
184
|
return `import { ${typeormImports.join(', ')} } from 'typeorm';${relImportsStr}
|
|
109
185
|
|
|
110
|
-
@Entity('${tableName}')
|
|
186
|
+
${indexDecorators}@Entity('${tableName}')
|
|
111
187
|
export class ${pascalName}OrmEntity {
|
|
112
188
|
@PrimaryGeneratedColumn()
|
|
113
189
|
id!: number;
|
|
114
190
|
|
|
115
|
-
${propsCode}${tenantField}
|
|
191
|
+
${propsCode}${tenantField}${softDeleteField}${autoFieldsStr}
|
|
116
192
|
}
|
|
117
193
|
`;
|
|
118
194
|
}
|
|
@@ -208,7 +284,7 @@ export class ${pascalName}RepositoryAdapter extends BaseRepository<${pascalName}
|
|
|
208
284
|
const paginated = await this.paginate(qb, filter, {
|
|
209
285
|
allowedFilters: [${allowedProperties.map(property => `'${property}'`).join(', ')}],
|
|
210
286
|
allowedSorts: [${allowedProperties.map(property => `'${property}'`).join(', ')}],
|
|
211
|
-
searchableFields: [${properties.filter(p => p.type === 'string').map(p => `'${p.name}'`).join(', ') || "'id'"}],
|
|
287
|
+
searchableFields: [${properties.filter(p => p.type === 'string' || p.type === 'text').map(p => `'${p.name}'`).join(', ') || "'id'"}],
|
|
212
288
|
});
|
|
213
289
|
|
|
214
290
|
return {
|
|
@@ -244,14 +320,87 @@ function nestDtoTemplate(name, properties) {
|
|
|
244
320
|
.map((p) => {
|
|
245
321
|
const relationship = p.type === 'relationship' ? (0, relationships_1.getRelationships)([p])[0] : undefined;
|
|
246
322
|
const isArray = relationship?.kind === 'many-to-many';
|
|
247
|
-
const
|
|
323
|
+
const type = p.type;
|
|
324
|
+
const isNumber = type === 'number' || type === 'decimal' || type === 'currency' || type === 'integer' || type === 'relationship';
|
|
325
|
+
const isEnum = type === 'enum' && p.enumOptions;
|
|
326
|
+
const exampleVal = isArray
|
|
327
|
+
? '[1, 2]'
|
|
328
|
+
: isNumber
|
|
329
|
+
? '1'
|
|
330
|
+
: type === 'boolean'
|
|
331
|
+
? 'true'
|
|
332
|
+
: isEnum
|
|
333
|
+
? `'${p.enumOptions[0]}'`
|
|
334
|
+
: "'value'";
|
|
335
|
+
const enumAttr = isEnum ? `, enum: ${JSON.stringify(p.enumOptions)}` : '';
|
|
336
|
+
const apiProperty = `@ApiProperty({ example: ${exampleVal}, description: '${p.label}'${isArray ? ', isArray: true' : ''}${enumAttr} })`;
|
|
337
|
+
const validators = [];
|
|
338
|
+
if (p.required) {
|
|
339
|
+
validators.push('@IsNotEmpty()');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
validators.push('@IsOptional()');
|
|
343
|
+
}
|
|
344
|
+
if (isArray) {
|
|
345
|
+
validators.push('@IsArray()');
|
|
346
|
+
}
|
|
347
|
+
else if (type === 'relationship') {
|
|
348
|
+
validators.push('@IsNumber()');
|
|
349
|
+
}
|
|
350
|
+
else if (type === 'number' || type === 'decimal' || type === 'currency') {
|
|
351
|
+
validators.push('@IsNumber()');
|
|
352
|
+
}
|
|
353
|
+
else if (type === 'integer') {
|
|
354
|
+
validators.push('@IsInt()');
|
|
355
|
+
}
|
|
356
|
+
else if (type === 'bigint') {
|
|
357
|
+
validators.push('@IsString()');
|
|
358
|
+
}
|
|
359
|
+
else if (type === 'boolean') {
|
|
360
|
+
validators.push('@IsBoolean()');
|
|
361
|
+
}
|
|
362
|
+
else if (type === 'datepicker' || type === 'datetime' || type === 'date') {
|
|
363
|
+
validators.push('@IsDateString()');
|
|
364
|
+
}
|
|
365
|
+
else if (type === 'json') {
|
|
366
|
+
validators.push('@IsObject()');
|
|
367
|
+
}
|
|
368
|
+
else if (type === 'email') {
|
|
369
|
+
validators.push('@IsEmail()');
|
|
370
|
+
}
|
|
371
|
+
else if (isEnum) {
|
|
372
|
+
validators.push(`@IsIn(${JSON.stringify(p.enumOptions)})`);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
validators.push('@IsString()');
|
|
376
|
+
}
|
|
248
377
|
const suffix = p.required ? '!' : '?';
|
|
249
|
-
|
|
378
|
+
let typeStr = 'string';
|
|
379
|
+
if (isArray) {
|
|
380
|
+
typeStr = 'number[]';
|
|
381
|
+
}
|
|
382
|
+
else if (isNumber) {
|
|
383
|
+
typeStr = 'number';
|
|
384
|
+
}
|
|
385
|
+
else if (type === 'boolean') {
|
|
386
|
+
typeStr = 'boolean';
|
|
387
|
+
}
|
|
388
|
+
else if (type === 'datepicker' || type === 'datetime' || type === 'date') {
|
|
389
|
+
typeStr = 'string'; // ISO Date strings no DTO
|
|
390
|
+
}
|
|
391
|
+
else if (type === 'json') {
|
|
392
|
+
typeStr = 'any';
|
|
393
|
+
}
|
|
394
|
+
else if (isEnum) {
|
|
395
|
+
typeStr = p.enumOptions.map(o => `'${o}'`).join(' | ');
|
|
396
|
+
}
|
|
250
397
|
const propertyName = relationship?.kind === 'many-to-many' ? relationship.relationName : p.name;
|
|
251
|
-
|
|
398
|
+
const valStr = validators.join('\n ');
|
|
399
|
+
return ` ${apiProperty}\n ${valStr}\n ${propertyName}${suffix}: ${typeStr};`;
|
|
252
400
|
})
|
|
253
401
|
.join('\n\n');
|
|
254
402
|
return `import { ApiProperty } from '@nestjs/swagger';
|
|
403
|
+
import { IsNotEmpty, IsOptional, IsString, IsNumber, IsBoolean, IsDateString, IsObject, IsArray, IsInt, IsEmail, IsIn } from 'class-validator';
|
|
255
404
|
|
|
256
405
|
export class Create${pascalName}Dto {
|
|
257
406
|
${createProps}
|
|
@@ -316,10 +465,23 @@ export class ${pascalName}Service {
|
|
|
316
465
|
}
|
|
317
466
|
`;
|
|
318
467
|
}
|
|
319
|
-
function nestControllerTemplate(name) {
|
|
468
|
+
function nestControllerTemplate(name, persistence) {
|
|
320
469
|
const pascalName = (0, names_1.kebabToPascal)(name);
|
|
321
470
|
const camelName = (0, names_1.kebabToCamel)(name);
|
|
322
|
-
|
|
471
|
+
const isAppendOnly = persistence?.mode === 'append-only';
|
|
472
|
+
const appendOnlyImports = isAppendOnly ? ', Put, Delete, MethodNotAllowedException' : '';
|
|
473
|
+
const appendOnlyMethods = isAppendOnly
|
|
474
|
+
? `\n\n @Put(':id')
|
|
475
|
+
override async update(): Promise<never> {
|
|
476
|
+
throw new MethodNotAllowedException('Updates not allowed for append-only entities.');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@Delete(':id')
|
|
480
|
+
override async delete(): Promise<never> {
|
|
481
|
+
throw new MethodNotAllowedException('Deletions not allowed for append-only entities.');
|
|
482
|
+
}`
|
|
483
|
+
: '';
|
|
484
|
+
return `import { Controller${appendOnlyImports} } from '@nestjs/common';
|
|
323
485
|
import { ApiTags } from '@nestjs/swagger';
|
|
324
486
|
import { BaseCrudController } from '@devstroupe/devkit-nest';
|
|
325
487
|
import { ${pascalName}Service } from '../../service/${name}.service';
|
|
@@ -331,7 +493,7 @@ import { Filter${pascalName}Dto } from './${name}.dto';
|
|
|
331
493
|
export class ${pascalName}Controller extends BaseCrudController<${pascalName}, Filter${pascalName}Dto> {
|
|
332
494
|
constructor(private readonly ${camelName}Service: ${pascalName}Service) {
|
|
333
495
|
super(${camelName}Service);
|
|
334
|
-
}
|
|
496
|
+
}${appendOnlyMethods}
|
|
335
497
|
}
|
|
336
498
|
`;
|
|
337
499
|
}
|
|
@@ -5,12 +5,25 @@ exports.nestDataSourceTemplate = nestDataSourceTemplate;
|
|
|
5
5
|
const names_1 = require("../shared/names");
|
|
6
6
|
const relationships_1 = require("../shared/relationships");
|
|
7
7
|
function columnType(property) {
|
|
8
|
-
|
|
8
|
+
const type = property.type;
|
|
9
|
+
if (type === 'number' || type === 'relationship' || type === 'integer')
|
|
9
10
|
return 'int';
|
|
10
|
-
if (
|
|
11
|
+
if (type === 'bigint')
|
|
12
|
+
return 'bigint';
|
|
13
|
+
if (type === 'boolean')
|
|
11
14
|
return 'boolean';
|
|
12
|
-
if (
|
|
15
|
+
if (type === 'datepicker' || type === 'datetime')
|
|
13
16
|
return 'datetime';
|
|
17
|
+
if (type === 'date')
|
|
18
|
+
return 'date';
|
|
19
|
+
if (type === 'decimal' || type === 'currency')
|
|
20
|
+
return 'decimal';
|
|
21
|
+
if (type === 'text')
|
|
22
|
+
return 'text';
|
|
23
|
+
if (type === 'enum')
|
|
24
|
+
return 'enum';
|
|
25
|
+
if (type === 'json')
|
|
26
|
+
return 'json';
|
|
14
27
|
return 'varchar';
|
|
15
28
|
}
|
|
16
29
|
function columnDefault(property) {
|
|
@@ -23,6 +36,10 @@ function columnDefault(property) {
|
|
|
23
36
|
return `, default: ${JSON.stringify(`'${String(property.default).replace(/'/g, "''")}'`)}`;
|
|
24
37
|
}
|
|
25
38
|
function targetTable(target, entities) {
|
|
39
|
+
if (target === 'user')
|
|
40
|
+
return 'users';
|
|
41
|
+
if (target === 'role')
|
|
42
|
+
return 'roles';
|
|
26
43
|
const entity = entities.find((candidate) => candidate.name === target);
|
|
27
44
|
return entity?.tableName ?? `${target}s`;
|
|
28
45
|
}
|
|
@@ -30,9 +47,36 @@ function nestMigrationTemplate(entity, entities, timestamp, properties = entity.
|
|
|
30
47
|
const className = `Create${(0, names_1.kebabToPascal)(entity.name)}${timestamp}`;
|
|
31
48
|
const tableName = entity.tableName ?? `${entity.name}s`;
|
|
32
49
|
const relationships = (0, relationships_1.getRelationships)(properties);
|
|
50
|
+
const isSoftDelete = entity.persistence?.deletion === 'soft-delete';
|
|
33
51
|
const scalarColumns = properties
|
|
34
52
|
.filter((property) => property.type !== 'relationship')
|
|
35
|
-
.map((property) =>
|
|
53
|
+
.map((property) => {
|
|
54
|
+
let extraStr = '';
|
|
55
|
+
if (property.type === 'decimal' || property.type === 'currency') {
|
|
56
|
+
const precision = property.precision !== undefined ? property.precision : 12;
|
|
57
|
+
const scale = property.scale !== undefined ? property.scale : 2;
|
|
58
|
+
extraStr = `, precision: ${precision}, scale: ${scale}`;
|
|
59
|
+
}
|
|
60
|
+
else if (property.type === 'enum' && property.enumOptions) {
|
|
61
|
+
extraStr = `, enum: ${JSON.stringify(property.enumOptions)}`;
|
|
62
|
+
}
|
|
63
|
+
return ` { name: '${property.name}', type: '${columnType(property)}'${extraStr}, isNullable: ${!property.required}${columnDefault(property)} }`;
|
|
64
|
+
});
|
|
65
|
+
if (isSoftDelete) {
|
|
66
|
+
scalarColumns.push(` { name: 'deleted_at', type: 'datetime', isNullable: true }`);
|
|
67
|
+
}
|
|
68
|
+
const autoFields = entity.automaticFields;
|
|
69
|
+
if (autoFields?.timestamps) {
|
|
70
|
+
scalarColumns.push(` { name: 'created_at', type: 'datetime', default: 'CURRENT_TIMESTAMP' }`);
|
|
71
|
+
scalarColumns.push(` { name: 'updated_at', type: 'datetime', default: 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' }`);
|
|
72
|
+
}
|
|
73
|
+
if (autoFields?.blamers) {
|
|
74
|
+
scalarColumns.push(` { name: 'created_by_user_id', type: 'int', isNullable: true }`);
|
|
75
|
+
scalarColumns.push(` { name: 'updated_by_user_id', type: 'int', isNullable: true }`);
|
|
76
|
+
}
|
|
77
|
+
if (autoFields?.optimisticLocking) {
|
|
78
|
+
scalarColumns.push(` { name: 'version', type: 'int', default: 1 }`);
|
|
79
|
+
}
|
|
36
80
|
const relationColumns = relationships
|
|
37
81
|
.filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
|
|
38
82
|
.map((relationship) => ` { name: '${relationship.foreignKey}', type: 'int', isNullable: ${!relationship.property.required}, isUnique: ${relationship.kind === 'one-to-one'} }`);
|
|
@@ -80,7 +124,37 @@ function nestMigrationTemplate(entity, entities, timestamp, properties = entity.
|
|
|
80
124
|
}), true);`,
|
|
81
125
|
};
|
|
82
126
|
});
|
|
83
|
-
|
|
127
|
+
const indexes = entity.indexes;
|
|
128
|
+
const checks = entity.checks;
|
|
129
|
+
const tableIndexes = indexes && indexes.length > 0
|
|
130
|
+
? indexes.map((idx) => {
|
|
131
|
+
const idxName = idx.name ?? `IDX_${tableName}_${idx.columns.join('_')}`;
|
|
132
|
+
return ` await queryRunner.createIndex('${tableName}', new TableIndex({
|
|
133
|
+
name: '${idxName}',
|
|
134
|
+
columnNames: ${JSON.stringify(idx.columns)},
|
|
135
|
+
isUnique: ${Boolean(idx.unique)}
|
|
136
|
+
}));`;
|
|
137
|
+
}).join('\n')
|
|
138
|
+
: '';
|
|
139
|
+
const dropIndexes = indexes && indexes.length > 0
|
|
140
|
+
? indexes.map((idx) => {
|
|
141
|
+
const idxName = idx.name ?? `IDX_${tableName}_${idx.columns.join('_')}`;
|
|
142
|
+
return ` await queryRunner.dropIndex('${tableName}', '${idxName}');`;
|
|
143
|
+
}).join('\n')
|
|
144
|
+
: '';
|
|
145
|
+
const checkConstraints = checks && checks.length > 0
|
|
146
|
+
? checks.map((chk, idx) => {
|
|
147
|
+
const checkName = `CHK_${tableName}_${idx}`;
|
|
148
|
+
return ` await queryRunner.query('ALTER TABLE \`${tableName}\` ADD CONSTRAINT \`${checkName}\` CHECK (${chk.replace(/'/g, "\\'")})');`;
|
|
149
|
+
}).join('\n')
|
|
150
|
+
: '';
|
|
151
|
+
const dropChecks = checks && checks.length > 0
|
|
152
|
+
? checks.map((chk, idx) => {
|
|
153
|
+
const checkName = `CHK_${tableName}_${idx}`;
|
|
154
|
+
return ` await queryRunner.query('ALTER TABLE \`${tableName}\` DROP CONSTRAINT \`${checkName}\`');`;
|
|
155
|
+
}).join('\n')
|
|
156
|
+
: '';
|
|
157
|
+
return `import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';
|
|
84
158
|
|
|
85
159
|
export class ${className} implements MigrationInterface {
|
|
86
160
|
name = '${className}';
|
|
@@ -96,12 +170,14 @@ ${foreignKeys ? `
|
|
|
96
170
|
await queryRunner.createForeignKeys('${tableName}', [
|
|
97
171
|
${foreignKeys}
|
|
98
172
|
]);` : ''}
|
|
173
|
+
${tableIndexes ? `\n${tableIndexes}` : ''}
|
|
174
|
+
${checkConstraints ? `\n${checkConstraints}` : ''}
|
|
99
175
|
${junctions.map((junction) => `
|
|
100
176
|
${junction.up}`).join('\n')}
|
|
101
177
|
}
|
|
102
178
|
|
|
103
179
|
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);
|
|
180
|
+
${dropChecks ? `${dropChecks}\n` : ''}${dropIndexes ? `${dropIndexes}\n` : ''}${junctions.slice().reverse().map((junction) => ` await queryRunner.dropTable('${junction.tableName}', true);`).join('\n')}${junctions.length > 0 ? '\n' : ''} await queryRunner.dropTable('${tableName}', true);
|
|
105
181
|
}
|
|
106
182
|
}
|
|
107
183
|
`;
|
|
@@ -17,17 +17,19 @@ function normalizeRelationship(property) {
|
|
|
17
17
|
throw new Error(`Relationship property "${property.name}" must define relation.target.`);
|
|
18
18
|
}
|
|
19
19
|
const config = property.relation;
|
|
20
|
+
const rawTarget = config.target;
|
|
21
|
+
const target = rawTarget.startsWith('$') ? rawTarget.slice(1) : rawTarget;
|
|
20
22
|
const kind = config.kind ?? 'many-to-one';
|
|
21
23
|
const collection = isCollectionRelationship(kind);
|
|
22
24
|
const relationName = config.relationName
|
|
23
|
-
?? (collection ? property.name :
|
|
25
|
+
?? (collection ? property.name : target);
|
|
24
26
|
const control = config.control
|
|
25
27
|
?? (kind === 'one-to-many' ? 'table' : kind === 'many-to-many' ? 'multi-select' : 'select');
|
|
26
28
|
const foreignKey = config.foreignKey
|
|
27
29
|
?? (isSingularRelationship(kind) ? property.name : undefined);
|
|
28
30
|
return {
|
|
29
31
|
property,
|
|
30
|
-
target
|
|
32
|
+
target,
|
|
31
33
|
kind,
|
|
32
34
|
control,
|
|
33
35
|
displayField: config.displayField ?? 'name',
|
|
@@ -61,7 +63,8 @@ function validateRelationships(entities) {
|
|
|
61
63
|
if (property.type !== 'relationship')
|
|
62
64
|
continue;
|
|
63
65
|
const relationship = normalizeRelationship(property);
|
|
64
|
-
|
|
66
|
+
const targetIsNative = ['user', 'role'].includes(relationship.target);
|
|
67
|
+
if (!entityNames.has(relationship.target) && !targetIsNative) {
|
|
65
68
|
throw new Error(`Entity "${entity.name}" relationship "${property.name}" targets unknown entity "${relationship.target}".`);
|
|
66
69
|
}
|
|
67
70
|
if (isSingularRelationship(relationship.kind) && relationship.foreignKey === relationship.relationName) {
|
|
@@ -76,7 +79,7 @@ function validateRelationships(entities) {
|
|
|
76
79
|
if (relationship.kind === 'one-to-many' && !relationship.foreignKey) {
|
|
77
80
|
throw new Error(`Entity "${entity.name}" one-to-many relationship "${property.name}" must define relation.foreignKey.`);
|
|
78
81
|
}
|
|
79
|
-
if (relationship.kind === 'one-to-many') {
|
|
82
|
+
if (relationship.kind === 'one-to-many' && !targetIsNative) {
|
|
80
83
|
const target = entities.find((candidate) => candidate.name === relationship.target);
|
|
81
84
|
const targetHasForeignKey = target?.properties.some((candidate) => candidate.name === relationship.foreignKey
|
|
82
85
|
|| (candidate.type === 'relationship'
|
|
@@ -24,3 +24,4 @@ __exportStar(require("./simple-list.template"), exports);
|
|
|
24
24
|
__exportStar(require("./dialog.template"), exports);
|
|
25
25
|
__exportStar(require("./badge.template"), exports);
|
|
26
26
|
__exportStar(require("./avatar.template"), exports);
|
|
27
|
+
__exportStar(require("./textarea.template"), exports);
|
|
@@ -25,6 +25,7 @@ export class DevkitInputComponent implements ControlValueAccessor {
|
|
|
25
25
|
@Input() label = '';
|
|
26
26
|
@Input() placeholder = '';
|
|
27
27
|
@Input() type = 'text';
|
|
28
|
+
@Input() prefix = '';
|
|
28
29
|
@Input() required = false;
|
|
29
30
|
@Input() disabled = false;
|
|
30
31
|
|
|
@@ -60,15 +61,22 @@ export class DevkitInputComponent implements ControlValueAccessor {
|
|
|
60
61
|
<label *ngIf="label" class="text-xs font-semibold text-muted-foreground">
|
|
61
62
|
{{ label }}<span *ngIf="required" class="text-destructive ml-0.5">*</span>
|
|
62
63
|
</label>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
<div class="relative flex items-center w-full">
|
|
65
|
+
<span *ngIf="prefix" class="absolute left-3 text-sm text-muted-foreground select-none pointer-events-none">
|
|
66
|
+
{{ prefix }}
|
|
67
|
+
</span>
|
|
68
|
+
<input
|
|
69
|
+
hlmInput
|
|
70
|
+
[type]="type"
|
|
71
|
+
[value]="value"
|
|
72
|
+
[placeholder]="placeholder"
|
|
73
|
+
[disabled]="disabled"
|
|
74
|
+
(input)="onInput($event)"
|
|
75
|
+
(blur)="onTouched()"
|
|
76
|
+
[class.pl-9]="prefix"
|
|
77
|
+
class="w-full"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
72
80
|
</div>
|
|
73
81
|
`
|
|
74
82
|
};
|