@devstroupe/devkit-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/boilerplates/angular-template/src/app/services/user.service.ts +12 -0
  2. package/dist/boilerplates/nest-template/src/app.module.ts +23 -0
  3. package/dist/boilerplates/nest-template/src/database/migrations/1600000000000-create-roles.ts +20 -0
  4. package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +13 -2
  5. package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +16 -0
  6. package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +44 -5
  7. package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage-download.controller.ts +38 -0
  8. package/dist/boilerplates/nest-template/src/modules/storage/storage.module.ts +7 -0
  9. package/dist/boilerplates/nest-template/src/modules/user/database-seed.service.ts +31 -18
  10. package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +1 -0
  11. package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +1 -0
  12. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +3 -0
  13. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +6 -0
  14. package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +56 -0
  15. package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +4 -0
  16. package/dist/boilerplates/nest-template/src/modules/user/user.module.ts +2 -0
  17. package/dist/index.js +24 -3
  18. package/dist/templates/angular/form.template.js +85 -9
  19. package/dist/templates/angular/list.template.js +27 -5
  20. package/dist/templates/nest/crud.templates.d.ts +16 -2
  21. package/dist/templates/nest/crud.templates.js +175 -13
  22. package/dist/templates/nest/migration.template.js +82 -6
  23. package/dist/templates/shared/relationships.js +7 -4
  24. package/dist/templates/ui/components/index.d.ts +1 -0
  25. package/dist/templates/ui/components/index.js +1 -0
  26. package/dist/templates/ui/components/input.template.js +17 -9
  27. package/dist/templates/ui/components/textarea.template.d.ts +4 -0
  28. package/dist/templates/ui/components/textarea.template.js +76 -0
  29. package/package.json +3 -3
@@ -38,13 +38,14 @@ function angularFormHtmlTemplate(name, properties, entities = []) {
38
38
  .filter((property) => property.type !== 'relationship' || (0, relationships_1.getRelationships)([property])[0].kind !== 'one-to-many')
39
39
  .map((property) => {
40
40
  const controlName = formControlName(property);
41
- if (property.type === 'boolean') {
41
+ const type = property.type;
42
+ if (type === 'boolean') {
42
43
  return ` <div *ngIf="!isFieldHidden('${controlName}')" class="flex items-center space-x-2 pt-2">
43
44
  <input type="checkbox" formControlName="${controlName}" id="${controlName}" class="h-4 w-4 rounded border-input bg-background text-primary focus:ring-ring">
44
45
  <label for="${controlName}" class="text-sm font-medium text-foreground">${property.label}</label>
45
46
  </div>`;
46
47
  }
47
- if (property.type === 'relationship') {
48
+ if (type === 'relationship') {
48
49
  const relationship = (0, relationships_1.getRelationships)([property])[0];
49
50
  const stateName = relationStateName(relationship);
50
51
  return ` <devkit-select
@@ -62,15 +63,81 @@ function angularFormHtmlTemplate(name, properties, entities = []) {
62
63
  [required]="${Boolean(property.required)}">
63
64
  </devkit-select>`;
64
65
  }
65
- const isNumber = property.type === 'number';
66
- return ` <devkit-input
66
+ if (type === 'text' || type === 'json') {
67
+ return ` <devkit-textarea
67
68
  *ngIf="!isFieldHidden('${controlName}')"
68
- ${isNumber ? 'type="number"' : ''}
69
+ formControlName="${controlName}"
70
+ label="${property.label}"
71
+ placeholder="Digite ${property.label.toLowerCase()}"
72
+ [required]="${Boolean(property.required)}">
73
+ </devkit-textarea>`;
74
+ }
75
+ if (type === 'enum') {
76
+ return ` <devkit-select
77
+ *ngIf="!isFieldHidden('${controlName}')"
78
+ formControlName="${controlName}"
79
+ label="${property.label}"
80
+ placeholder="Selecione ${property.label.toLowerCase()}"
81
+ [options]="${controlName}Options"
82
+ [required]="${Boolean(property.required)}">
83
+ </devkit-select>`;
84
+ }
85
+ if (type === 'string' ||
86
+ type === 'number' ||
87
+ type === 'decimal' ||
88
+ type === 'currency' ||
89
+ type === 'integer' ||
90
+ type === 'bigint' ||
91
+ type === 'email' ||
92
+ type === 'phone' ||
93
+ type === 'document' ||
94
+ type === 'color' ||
95
+ type === 'datepicker' ||
96
+ type === 'date' ||
97
+ type === 'datetime' ||
98
+ type === 'file') {
99
+ let typeAttr = 'type="text"';
100
+ let stepAttr = '';
101
+ let prefixAttr = '';
102
+ if (type === 'number' || type === 'integer') {
103
+ typeAttr = 'type="number"';
104
+ }
105
+ else if (type === 'decimal') {
106
+ typeAttr = 'type="number"';
107
+ stepAttr = 'step="any"';
108
+ }
109
+ else if (type === 'currency') {
110
+ typeAttr = 'type="number"';
111
+ stepAttr = 'step="0.01"';
112
+ prefixAttr = 'prefix="R$"';
113
+ }
114
+ else if (type === 'email') {
115
+ typeAttr = 'type="email"';
116
+ }
117
+ else if (type === 'color') {
118
+ typeAttr = 'type="color"';
119
+ }
120
+ else if (type === 'date' || type === 'datepicker') {
121
+ typeAttr = 'type="date"';
122
+ }
123
+ else if (type === 'datetime') {
124
+ typeAttr = 'type="datetime-local"';
125
+ }
126
+ else if (type === 'file') {
127
+ typeAttr = 'type="file"';
128
+ }
129
+ return ` <devkit-input
130
+ *ngIf="!isFieldHidden('${controlName}')"
131
+ ${typeAttr}
132
+ ${stepAttr}
133
+ ${prefixAttr}
69
134
  formControlName="${controlName}"
70
135
  label="${property.label}"
71
136
  placeholder="Digite ${property.label.toLowerCase()}"
72
137
  [required]="${Boolean(property.required)}">
73
138
  </devkit-input>`;
139
+ }
140
+ return '';
74
141
  })
75
142
  .join('\n\n');
76
143
  const dependentSections = dependentRelationships.map((relationship) => {
@@ -155,8 +222,9 @@ function angularFormTsTemplate(name, properties, entities = []) {
155
222
  const relationships = (0, relationships_1.getRelationships)(properties);
156
223
  const optionRelationships = relationships.filter((relationship) => relationship.kind !== 'one-to-many');
157
224
  const dependents = relationships.filter((relationship) => relationship.kind === 'one-to-many' && relationship.embedded);
158
- const usesInput = properties.some((property) => property.type === 'string' || property.type === 'number' || property.type === 'datepicker');
159
- const usesSelect = optionRelationships.length > 0;
225
+ const usesInput = properties.some((p) => p.type === 'string' || p.type === 'number' || p.type === 'decimal' || p.type === 'currency' || p.type === 'datepicker');
226
+ const usesTextarea = properties.some((property) => property.type === 'text');
227
+ const usesSelect = optionRelationships.length > 0 || properties.some((property) => property.type === 'enum');
160
228
  const formGroupProps = properties
161
229
  .filter((property) => property.type !== 'relationship' || (0, relationships_1.getRelationships)([property])[0].kind !== 'one-to-many')
162
230
  .map((property) => {
@@ -198,6 +266,13 @@ function angularFormTsTemplate(name, properties, entities = []) {
198
266
  minSearchLength: ${relationship.minSearchLength}
199
267
  });`;
200
268
  }).join('\n');
269
+ const enumOptionsState = properties
270
+ .filter((p) => p.type === 'enum' && p.enumOptions)
271
+ .map((p) => {
272
+ const options = p.enumOptions.map(o => ({ label: o, value: o }));
273
+ return ` readonly ${p.name}Options = ${JSON.stringify(options)};`;
274
+ })
275
+ .join('\n');
201
276
  const dependentState = dependents.map((relationship) => {
202
277
  const stateName = relationStateName(relationship);
203
278
  const columns = relationshipColumns(relationship, entities);
@@ -315,6 +390,7 @@ import { CommonModule } from '@angular/common';
315
390
  import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
316
391
  import { ActivatedRoute, Router } from '@angular/router';
317
392
  ${usesInput ? "import { DevkitInputComponent } from '@devstroupe/ui/input/input.component';" : ''}
393
+ ${usesTextarea ? "import { DevkitTextareaComponent } from '@devstroupe/ui/textarea/textarea.component';" : ''}
318
394
  ${usesSelect ? "import { DevkitSelectComponent } from '@devstroupe/ui/select/select.component';" : ''}
319
395
  import { DevkitButtonComponent } from '@devstroupe/ui/button/button.component';
320
396
  import { HlmCardImports } from '@spartan-ng/helm/card';
@@ -328,7 +404,7 @@ import { ${pascalName}Service } from '../../../services/${name}.service';
328
404
  @Component({
329
405
  selector: 'app-${name}-form',
330
406
  standalone: true,
331
- imports: [CommonModule, ReactiveFormsModule${usesInput ? ', DevkitInputComponent' : ''}${usesSelect ? ', DevkitSelectComponent' : ''}, DevkitButtonComponent, HlmCardImports, HlmSkeletonImports${componentImports.map((item) => `, ${item}`).join('')}],
407
+ imports: [CommonModule, ReactiveFormsModule${usesInput ? ', DevkitInputComponent' : ''}${usesTextarea ? ', DevkitTextareaComponent' : ''}${usesSelect ? ', DevkitSelectComponent' : ''}, DevkitButtonComponent, HlmCardImports, HlmSkeletonImports${componentImports.map((item) => `, ${item}`).join('')}],
332
408
  templateUrl: './${name}-form.component.html'
333
409
  })
334
410
  export class ${pascalName}FormComponent implements OnInit${optionRelationships.length > 0 ? ', OnDestroy' : ''} {
@@ -351,7 +427,7 @@ ${relationServiceInjections}
351
427
  isEdit = false;
352
428
  isFetching = false;
353
429
  loading = false;
354
- ${optionState}
430
+ ${optionState}${enumOptionsState ? '\n' + enumOptionsState : ''}
355
431
  ${dependentState}
356
432
 
357
433
  ngOnInit(): void {
@@ -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 (p.type === 'boolean')
74
+ if (type === 'boolean')
62
75
  typeStr = 'boolean';
63
- if (p.type === 'datepicker')
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): string;
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): 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
- const typeStr = p.type === 'number' ? 'number' : p.type === 'boolean' ? 'boolean' : 'string';
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
- const decorator = p.required ? '@Column()' : '@Column({ nullable: true })';
102
- const typeStr = p.type === 'number' ? 'number' : p.type === 'boolean' ? 'boolean' : 'string';
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 apiProperty = `@ApiProperty({ example: ${isArray ? '[1, 2]' : p.type === 'number' || p.type === 'relationship' ? '1' : p.type === 'boolean' ? 'true' : "'value'"}, description: '${p.label}'${isArray ? ', isArray: true' : ''} })`;
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
- const typeStr = isArray ? 'number[]' : p.type === 'number' || p.type === 'relationship' ? 'number' : p.type === 'boolean' ? 'boolean' : 'string';
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
- return ` ${apiProperty}\n ${propertyName}${suffix}: ${typeStr};`;
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
- return `import { Controller } from '@nestjs/common';
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
- if (property.type === 'number' || property.type === 'relationship')
8
+ const type = property.type;
9
+ if (type === 'number' || type === 'relationship' || type === 'integer')
9
10
  return 'int';
10
- if (property.type === 'boolean')
11
+ if (type === 'bigint')
12
+ return 'bigint';
13
+ if (type === 'boolean')
11
14
  return 'boolean';
12
- if (property.type === 'datepicker')
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) => ` { name: '${property.name}', type: '${columnType(property)}', isNullable: ${!property.required}${columnDefault(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
- return `import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
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
  `;