create-backlist 10.0.9 → 10.1.1
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/bin/index.js +1411 -1060
- package/package.json +1 -1
- package/src/generators/dotnet.js +137 -81
- package/src/generators/java.js +118 -130
- package/src/generators/js.js +199 -207
- package/src/generators/nestjs.js +168 -155
- package/src/generators/node.js +212 -194
- package/src/generators/python.js +130 -45
- package/src/generators/template.js +47 -2
- package/src/qa/qa-engine.js +2320 -414
- package/src/templates/dotnet/partials/Controller.cs.ejs +264 -16
- package/src/templates/dotnet/partials/DbContext.cs.ejs +93 -3
- package/src/templates/dotnet/partials/Model.cs.ejs +192 -31
|
@@ -1,24 +1,272 @@
|
|
|
1
|
+
// Auto-generated by create-backlist — DO NOT EDIT MANUALLY
|
|
1
2
|
using Microsoft.AspNetCore.Mvc;
|
|
3
|
+
using Microsoft.EntityFrameworkCore;
|
|
4
|
+
using <%= projectName %>.Data;
|
|
5
|
+
using <%= projectName %>.Models;
|
|
6
|
+
using <%= projectName %>.DTOs;
|
|
2
7
|
|
|
3
8
|
namespace <%= projectName %>.Controllers;
|
|
4
9
|
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// CRUD controller for <b><%= controllerName %></b> — auto-generated by Backlist.
|
|
12
|
+
/// </summary>
|
|
5
13
|
[ApiController]
|
|
6
14
|
[Route("api/[controller]")]
|
|
15
|
+
[Produces("application/json")]
|
|
7
16
|
public class <%= controllerName %>Controller : ControllerBase
|
|
8
17
|
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
private readonly ApplicationDbContext _db;
|
|
19
|
+
private readonly ILogger<<%= controllerName %>Controller> _logger;
|
|
20
|
+
|
|
21
|
+
public <%= controllerName %>Controller(ApplicationDbContext db, ILogger<<%= controllerName %>Controller> logger)
|
|
22
|
+
{
|
|
23
|
+
_db = db;
|
|
24
|
+
_logger = logger;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// GET api/<%= controllerName.toLowerCase() %>?page=1&pageSize=20&search=...
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
30
|
+
/// <summary>Returns a paginated list of <%= controllerName %> records.</summary>
|
|
31
|
+
[HttpGet]
|
|
32
|
+
[ProducesResponseType(typeof(PagedResult<<%= controllerName %>>), StatusCodes.Status200OK)]
|
|
33
|
+
public async Task<IActionResult> GetAll(
|
|
34
|
+
[FromQuery] int page = 1,
|
|
35
|
+
[FromQuery] int pageSize = 20,
|
|
36
|
+
[FromQuery] string? search = null)
|
|
37
|
+
{
|
|
38
|
+
if (page < 1) page = 1;
|
|
39
|
+
if (pageSize < 1 || pageSize > 100) pageSize = 20;
|
|
40
|
+
|
|
41
|
+
try
|
|
42
|
+
{
|
|
43
|
+
var query = _db.<%= controllerName %>s.AsNoTracking();
|
|
44
|
+
|
|
45
|
+
// Generic search across string properties
|
|
46
|
+
if (!string.IsNullOrWhiteSpace(search))
|
|
47
|
+
{
|
|
48
|
+
var s = search.ToLower();
|
|
49
|
+
query = query.Where(e =>
|
|
50
|
+
EF.Functions.Like(e.Id.ToString(), $"%{s}%")
|
|
51
|
+
<% if (typeof endpoints !== 'undefined') { endpoints.forEach(ep => { %> // extend with real string fields after scaffolding <% }); } %>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var total = await query.CountAsync();
|
|
56
|
+
var items = await query
|
|
57
|
+
.OrderByDescending(e => e.CreatedAt)
|
|
58
|
+
.Skip((page - 1) * pageSize)
|
|
59
|
+
.Take(pageSize)
|
|
60
|
+
.ToListAsync();
|
|
61
|
+
|
|
62
|
+
return Ok(new PagedResult<<%= controllerName %>>
|
|
63
|
+
{
|
|
64
|
+
Items = items,
|
|
65
|
+
Total = total,
|
|
66
|
+
Page = page,
|
|
67
|
+
PageSize = pageSize,
|
|
68
|
+
TotalPages = (int)Math.Ceiling(total / (double)pageSize)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (Exception ex)
|
|
72
|
+
{
|
|
73
|
+
_logger.LogError(ex, "Error fetching <%= controllerName %> list");
|
|
74
|
+
return StatusCode(500, new { error = "Internal server error." });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// GET api/<%= controllerName.toLowerCase() %>/{id}
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
81
|
+
/// <summary>Returns a single <%= controllerName %> by GUID.</summary>
|
|
82
|
+
[HttpGet("{id:guid}")]
|
|
83
|
+
[ProducesResponseType(typeof(<%= controllerName %>), StatusCodes.Status200OK)]
|
|
84
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
85
|
+
public async Task<IActionResult> GetById(Guid id)
|
|
86
|
+
{
|
|
87
|
+
var entity = await _db.<%= controllerName %>s.AsNoTracking()
|
|
88
|
+
.FirstOrDefaultAsync(e => e.Id == id);
|
|
89
|
+
|
|
90
|
+
return entity is null
|
|
91
|
+
? NotFound(new { error = $"<%= controllerName %> with id '{id}' not found." })
|
|
92
|
+
: Ok(entity);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// POST api/<%= controllerName.toLowerCase() %>
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
98
|
+
/// <summary>Creates a new <%= controllerName %>.</summary>
|
|
99
|
+
[HttpPost]
|
|
100
|
+
[ProducesResponseType(typeof(<%= controllerName %>), StatusCodes.Status201Created)]
|
|
101
|
+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
102
|
+
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
|
103
|
+
public async Task<IActionResult> Create([FromBody] Create<%= controllerName %>Dto dto)
|
|
104
|
+
{
|
|
105
|
+
if (!ModelState.IsValid)
|
|
106
|
+
return BadRequest(ModelState);
|
|
107
|
+
|
|
108
|
+
try
|
|
109
|
+
{
|
|
110
|
+
var entity = dto.ToEntity();
|
|
111
|
+
_db.<%= controllerName %>s.Add(entity);
|
|
112
|
+
await _db.SaveChangesAsync();
|
|
113
|
+
|
|
114
|
+
_logger.LogInformation("<%= controllerName %> created: {Id}", entity.Id);
|
|
115
|
+
return CreatedAtAction(nameof(GetById), new { id = entity.Id }, entity);
|
|
116
|
+
}
|
|
117
|
+
catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("unique", StringComparison.OrdinalIgnoreCase) == true)
|
|
118
|
+
{
|
|
119
|
+
return Conflict(new { error = "A record with this data already exists." });
|
|
120
|
+
}
|
|
121
|
+
catch (Exception ex)
|
|
122
|
+
{
|
|
123
|
+
_logger.LogError(ex, "Error creating <%= controllerName %>");
|
|
124
|
+
return StatusCode(500, new { error = "Internal server error." });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
129
|
+
// PUT api/<%= controllerName.toLowerCase() %>/{id}
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
131
|
+
/// <summary>Fully updates an existing <%= controllerName %>.</summary>
|
|
132
|
+
[HttpPut("{id:guid}")]
|
|
133
|
+
[ProducesResponseType(typeof(<%= controllerName %>), StatusCodes.Status200OK)]
|
|
134
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
135
|
+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
136
|
+
public async Task<IActionResult> Update(Guid id, [FromBody] Update<%= controllerName %>Dto dto)
|
|
137
|
+
{
|
|
138
|
+
if (!ModelState.IsValid)
|
|
139
|
+
return BadRequest(ModelState);
|
|
140
|
+
|
|
141
|
+
var entity = await _db.<%= controllerName %>s.FindAsync(id);
|
|
142
|
+
if (entity is null)
|
|
143
|
+
return NotFound(new { error = $"<%= controllerName %> with id '{id}' not found." });
|
|
144
|
+
|
|
145
|
+
try
|
|
146
|
+
{
|
|
147
|
+
dto.ApplyTo(entity);
|
|
148
|
+
entity.UpdatedAt = DateTime.UtcNow;
|
|
149
|
+
await _db.SaveChangesAsync();
|
|
150
|
+
|
|
151
|
+
_logger.LogInformation("<%= controllerName %> updated: {Id}", id);
|
|
152
|
+
return Ok(entity);
|
|
153
|
+
}
|
|
154
|
+
catch (Exception ex)
|
|
155
|
+
{
|
|
156
|
+
_logger.LogError(ex, "Error updating <%= controllerName %> {Id}", id);
|
|
157
|
+
return StatusCode(500, new { error = "Internal server error." });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
162
|
+
// PATCH api/<%= controllerName.toLowerCase() %>/{id}
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
164
|
+
/// <summary>Partially updates an existing <%= controllerName %>.</summary>
|
|
165
|
+
[HttpPatch("{id:guid}")]
|
|
166
|
+
[ProducesResponseType(typeof(<%= controllerName %>), StatusCodes.Status200OK)]
|
|
167
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
168
|
+
public async Task<IActionResult> Patch(Guid id, [FromBody] Patch<%= controllerName %>Dto dto)
|
|
169
|
+
{
|
|
170
|
+
var entity = await _db.<%= controllerName %>s.FindAsync(id);
|
|
171
|
+
if (entity is null)
|
|
172
|
+
return NotFound(new { error = $"<%= controllerName %> with id '{id}' not found." });
|
|
173
|
+
|
|
174
|
+
try
|
|
175
|
+
{
|
|
176
|
+
dto.ApplyPartialTo(entity);
|
|
177
|
+
entity.UpdatedAt = DateTime.UtcNow;
|
|
178
|
+
await _db.SaveChangesAsync();
|
|
179
|
+
|
|
180
|
+
return Ok(entity);
|
|
181
|
+
}
|
|
182
|
+
catch (Exception ex)
|
|
183
|
+
{
|
|
184
|
+
_logger.LogError(ex, "Error patching <%= controllerName %> {Id}", id);
|
|
185
|
+
return StatusCode(500, new { error = "Internal server error." });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// DELETE api/<%= controllerName.toLowerCase() %>/{id}
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
192
|
+
/// <summary>Soft-deletes a <%= controllerName %> (sets DeletedAt timestamp).</summary>
|
|
193
|
+
[HttpDelete("{id:guid}")]
|
|
194
|
+
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
195
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
196
|
+
public async Task<IActionResult> Delete(Guid id)
|
|
197
|
+
{
|
|
198
|
+
var entity = await _db.<%= controllerName %>s.FindAsync(id);
|
|
199
|
+
if (entity is null)
|
|
200
|
+
return NotFound(new { error = $"<%= controllerName %> with id '{id}' not found." });
|
|
201
|
+
|
|
202
|
+
try
|
|
203
|
+
{
|
|
204
|
+
// Soft delete — set DeletedAt instead of removing row
|
|
205
|
+
entity.DeletedAt = DateTime.UtcNow;
|
|
206
|
+
entity.UpdatedAt = DateTime.UtcNow;
|
|
207
|
+
await _db.SaveChangesAsync();
|
|
208
|
+
|
|
209
|
+
_logger.LogInformation("<%= controllerName %> soft-deleted: {Id}", id);
|
|
210
|
+
return NoContent();
|
|
211
|
+
}
|
|
212
|
+
catch (Exception ex)
|
|
213
|
+
{
|
|
214
|
+
_logger.LogError(ex, "Error deleting <%= controllerName %> {Id}", id);
|
|
215
|
+
return StatusCode(500, new { error = "Internal server error." });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
220
|
+
// DELETE api/<%= controllerName.toLowerCase() %>/{id}/hard
|
|
221
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
222
|
+
/// <summary>Permanently removes a <%= controllerName %> record from the database.</summary>
|
|
223
|
+
[HttpDelete("{id:guid}/hard")]
|
|
224
|
+
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
225
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
226
|
+
public async Task<IActionResult> HardDelete(Guid id)
|
|
227
|
+
{
|
|
228
|
+
var entity = await _db.<%= controllerName %>s.FindAsync(id);
|
|
229
|
+
if (entity is null)
|
|
230
|
+
return NotFound(new { error = $"<%= controllerName %> with id '{id}' not found." });
|
|
231
|
+
|
|
232
|
+
_db.<%= controllerName %>s.Remove(entity);
|
|
233
|
+
await _db.SaveChangesAsync();
|
|
234
|
+
|
|
235
|
+
_logger.LogWarning("<%= controllerName %> permanently deleted: {Id}", id);
|
|
236
|
+
return NoContent();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
240
|
+
// GET api/<%= controllerName.toLowerCase() %>/count
|
|
241
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
242
|
+
/// <summary>Returns total count of active <%= controllerName %> records.</summary>
|
|
243
|
+
[HttpGet("count")]
|
|
244
|
+
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
|
245
|
+
public async Task<IActionResult> Count()
|
|
246
|
+
{
|
|
247
|
+
var count = await _db.<%= controllerName %>s.AsNoTracking()
|
|
248
|
+
.Where(e => e.DeletedAt == null)
|
|
249
|
+
.CountAsync();
|
|
250
|
+
return Ok(new { count });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
<% if (typeof endpoints !== 'undefined') { endpoints.forEach(endpoint => {
|
|
254
|
+
const routePath = endpoint.path
|
|
255
|
+
.replace('/api/' + controllerName.toLowerCase(), '')
|
|
256
|
+
.replace(/^\//, '');
|
|
257
|
+
const isStandard = ['GET','POST','PUT','PATCH','DELETE'].includes(endpoint.method.toUpperCase())
|
|
258
|
+
&& (routePath === '' || routePath === '{id}' || routePath === '{id:guid}');
|
|
259
|
+
if (!isStandard) { %>
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
261
|
+
// <%= endpoint.method.toUpperCase() %> api/<%= controllerName.toLowerCase() %>/<%= routePath %>
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
263
|
+
/// <summary>Auto-generated endpoint: <%= endpoint.method.toUpperCase() %> <%= endpoint.path %></summary>
|
|
264
|
+
[Http<%= endpoint.method.charAt(0).toUpperCase() + endpoint.method.slice(1).toLowerCase() %>("<%= routePath %>")]
|
|
265
|
+
public async Task<IActionResult> Custom_<%= endpoint.method.toUpperCase() %>_<%= routePath.replace(/\{|\}/g, '').replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '') || 'Index' %>()
|
|
266
|
+
{
|
|
267
|
+
// TODO: Implement custom logic for <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>
|
|
268
|
+
await Task.CompletedTask;
|
|
269
|
+
return Ok(new { message = "Endpoint reached: <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>" });
|
|
270
|
+
}
|
|
271
|
+
<% } }); } %>
|
|
272
|
+
}
|
|
@@ -1,15 +1,105 @@
|
|
|
1
|
-
// Auto-generated by create-backlist
|
|
1
|
+
// Auto-generated by create-backlist — DO NOT EDIT MANUALLY
|
|
2
2
|
using Microsoft.EntityFrameworkCore;
|
|
3
|
+
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
3
4
|
using <%= projectName %>.Models;
|
|
4
5
|
|
|
5
6
|
namespace <%= projectName %>.Data
|
|
6
7
|
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Main EF Core DbContext for <b><%= projectName %></b>.
|
|
10
|
+
/// Includes: soft-delete global filter, automatic audit timestamps,
|
|
11
|
+
/// unique indexes, and seed-data scaffolding hooks.
|
|
12
|
+
/// </summary>
|
|
7
13
|
public class ApplicationDbContext : DbContext
|
|
8
14
|
{
|
|
9
|
-
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
|
15
|
+
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
|
16
|
+
: base(options) { }
|
|
10
17
|
|
|
18
|
+
// ── DbSets ────────────────────────────────────────────────────────────
|
|
11
19
|
<% modelsToGenerate.forEach(model => { %>
|
|
20
|
+
/// <summary>Represents the <b><%= model.name %></b> table.</summary>
|
|
12
21
|
public DbSet<<%= model.name %>> <%= model.name %>s { get; set; }
|
|
13
22
|
<% }); %>
|
|
23
|
+
|
|
24
|
+
// ── Model Configuration ───────────────────────────────────────────────
|
|
25
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
26
|
+
{
|
|
27
|
+
base.OnModelCreating(modelBuilder);
|
|
28
|
+
|
|
29
|
+
<% modelsToGenerate.forEach(model => { %>
|
|
30
|
+
// ── <%= model.name %> ──────────────────────────────────────────────
|
|
31
|
+
modelBuilder.Entity<<%= model.name %>>(entity =>
|
|
32
|
+
{
|
|
33
|
+
entity.HasKey(e => e.Id);
|
|
34
|
+
|
|
35
|
+
// Soft-delete: only return rows where DeletedAt is null
|
|
36
|
+
entity.HasQueryFilter(e => e.DeletedAt == null);
|
|
37
|
+
|
|
38
|
+
// Unique indexes
|
|
39
|
+
<% (model.fields || []).forEach(field => {
|
|
40
|
+
if (field.isUnique) { %>
|
|
41
|
+
entity.HasIndex(e => e.<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>)
|
|
42
|
+
.IsUnique()
|
|
43
|
+
.HasDatabaseName("IX_<%= model.name %>_<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>");
|
|
44
|
+
<% } }); %>
|
|
45
|
+
|
|
46
|
+
// Column precision for decimal fields
|
|
47
|
+
<% (model.fields || []).forEach(field => {
|
|
48
|
+
const n = String(field.name || '').toLowerCase();
|
|
49
|
+
const isDecimal = field.type === 'Number' && (n.includes('price') || n.includes('amount') || n.includes('total') || n.includes('cost'));
|
|
50
|
+
if (isDecimal) { %>
|
|
51
|
+
entity.Property(e => e.<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>)
|
|
52
|
+
.HasColumnType("decimal(18,2)");
|
|
53
|
+
<% } }); %>
|
|
54
|
+
|
|
55
|
+
// Audit timestamp defaults
|
|
56
|
+
entity.Property(e => e.CreatedAt).HasDefaultValueSql("NOW()");
|
|
57
|
+
entity.Property(e => e.UpdatedAt).HasDefaultValueSql("NOW()");
|
|
58
|
+
|
|
59
|
+
// Table name override (snake_case convention)
|
|
60
|
+
entity.ToTable("<%= model.name.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '') %>s");
|
|
61
|
+
});
|
|
62
|
+
<% }); %>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Automatic Audit: SaveChanges / SaveChangesAsync ───────────────────
|
|
66
|
+
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
|
67
|
+
{
|
|
68
|
+
ApplyAuditInfo();
|
|
69
|
+
return base.SaveChanges(acceptAllChangesOnSuccess);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public override Task<int> SaveChangesAsync(
|
|
73
|
+
bool acceptAllChangesOnSuccess,
|
|
74
|
+
CancellationToken cancellationToken = default)
|
|
75
|
+
{
|
|
76
|
+
ApplyAuditInfo();
|
|
77
|
+
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private void ApplyAuditInfo()
|
|
81
|
+
{
|
|
82
|
+
var now = DateTime.UtcNow;
|
|
83
|
+
|
|
84
|
+
foreach (EntityEntry entry in ChangeTracker.Entries())
|
|
85
|
+
{
|
|
86
|
+
if (entry.State == EntityState.Added)
|
|
87
|
+
{
|
|
88
|
+
if (entry.Properties.Any(p => p.Metadata.Name == "CreatedAt"))
|
|
89
|
+
entry.Property("CreatedAt").CurrentValue = now;
|
|
90
|
+
if (entry.Properties.Any(p => p.Metadata.Name == "UpdatedAt"))
|
|
91
|
+
entry.Property("UpdatedAt").CurrentValue = now;
|
|
92
|
+
}
|
|
93
|
+
else if (entry.State == EntityState.Modified)
|
|
94
|
+
{
|
|
95
|
+
if (entry.Properties.Any(p => p.Metadata.Name == "UpdatedAt"))
|
|
96
|
+
entry.Property("UpdatedAt").CurrentValue = now;
|
|
97
|
+
|
|
98
|
+
// Prevent CreatedAt from being overwritten on update
|
|
99
|
+
if (entry.Properties.Any(p => p.Metadata.Name == "CreatedAt"))
|
|
100
|
+
entry.Property("CreatedAt").IsModified = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
14
104
|
}
|
|
15
|
-
}
|
|
105
|
+
}
|
|
@@ -1,51 +1,212 @@
|
|
|
1
|
-
// Auto-generated by create-backlist
|
|
2
|
-
using System;
|
|
1
|
+
// Auto-generated by create-backlist — DO NOT EDIT MANUALLY
|
|
3
2
|
using System.ComponentModel.DataAnnotations;
|
|
3
|
+
using System.ComponentModel.DataAnnotations.Schema;
|
|
4
4
|
|
|
5
5
|
namespace <%= projectName %>.Models
|
|
6
6
|
{
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Entity model for <b><%= modelName %></b> — auto-generated by Backlist.
|
|
9
|
+
/// </summary>
|
|
7
10
|
public class <%= modelName %>
|
|
8
11
|
{
|
|
12
|
+
// ── Primary Key ───────────────────────────────────────────────────────
|
|
9
13
|
[Key]
|
|
14
|
+
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
10
15
|
public Guid Id { get; set; } = Guid.NewGuid();
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
<%
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Small heuristic: money-like fields -> decimal
|
|
20
|
-
const n = String(field.name || '').toLowerCase();
|
|
21
|
-
if (field.type === 'Number' && (n.includes('price') || n.includes('amount') || n.includes('total'))) {
|
|
22
|
-
csharpType = 'decimal';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Required/Unique hints
|
|
26
|
-
const isEmail = n.includes('email');
|
|
27
|
-
%>
|
|
17
|
+
// ── Domain Fields ─────────────────────────────────────────────────────
|
|
18
|
+
<% (model.fields || []).forEach(field => {
|
|
19
|
+
// ── Type mapping ────────────────────────────────────────────────
|
|
20
|
+
let csharpType = 'string';
|
|
21
|
+
let defaultValue = '= string.Empty;';
|
|
22
|
+
let columnType = '';
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
// NOTE: Unique constraint should be configured in DbContext (HasIndex().IsUnique()).
|
|
31
|
-
<% } %>
|
|
24
|
+
const n = String(field.name || '').toLowerCase();
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
if (field.type === 'Number') {
|
|
27
|
+
const isDecimal = n.includes('price') || n.includes('amount') || n.includes('total') || n.includes('cost') || n.includes('fee') || n.includes('balance');
|
|
28
|
+
const isLong = n.includes('count') || n.includes('views') || n.includes('hits');
|
|
29
|
+
csharpType = isDecimal ? 'decimal' : (isLong ? 'long' : 'int');
|
|
30
|
+
columnType = isDecimal ? '[Column(TypeName = "decimal(18,2)")]' : '';
|
|
31
|
+
defaultValue = '';
|
|
32
|
+
} else if (field.type === 'Boolean') {
|
|
33
|
+
csharpType = 'bool';
|
|
34
|
+
defaultValue = '= false;';
|
|
35
|
+
} else if (field.type === 'Date' || field.type === 'DateTime') {
|
|
36
|
+
csharpType = 'DateTime?';
|
|
37
|
+
defaultValue = '';
|
|
38
|
+
} else if (field.type === 'Float' || field.type === 'Double') {
|
|
39
|
+
csharpType = 'double';
|
|
40
|
+
defaultValue = '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isEmail = n.includes('email');
|
|
44
|
+
const isPhone = n.includes('phone') || n.includes('mobile') || n.includes('tel');
|
|
45
|
+
const isUrl = n.includes('url') || n.includes('link') || n.includes('website');
|
|
46
|
+
const isPassword = n.includes('password') || n.includes('hash');
|
|
47
|
+
const maxLen = isPassword ? 512 : (isUrl ? 2048 : 256);
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
public <%= csharpType %> <%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %> { get; set; }
|
|
49
|
+
const propName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
50
|
+
%>
|
|
51
|
+
<% if (field.isUnique) { %>
|
|
52
|
+
// Unique index configured in ApplicationDbContext.OnModelCreating
|
|
42
53
|
<% } %>
|
|
54
|
+
<% if (isEmail) { %>[EmailAddress]<% } %>
|
|
55
|
+
<% if (isUrl) { %>[Url]<% } %>
|
|
56
|
+
<% if (isPhone) { %>[Phone]<% } %>
|
|
57
|
+
<% if (csharpType === 'string' && !isPassword) { %>[MaxLength(<%= maxLen %>)]<% } %>
|
|
58
|
+
<% if (csharpType === 'string') { %>[Required(ErrorMessage = "<%= propName %> is required.")]<% } %>
|
|
59
|
+
<% if (columnType) { %><%= columnType %><% } %>
|
|
60
|
+
public <%= csharpType %> <%= propName %> { get; set; } <%= defaultValue %>
|
|
43
61
|
|
|
44
62
|
<% }); %>
|
|
45
63
|
|
|
64
|
+
// ── Audit Fields ──────────────────────────────────────────────────────
|
|
65
|
+
/// <summary>UTC timestamp set on row creation.</summary>
|
|
46
66
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
/// <summary>UTC timestamp updated on every write (auto-managed by DbContext).</summary>
|
|
49
69
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
|
70
|
+
|
|
71
|
+
/// <summary>Soft-delete timestamp. NULL means the record is active.</summary>
|
|
72
|
+
public DateTime? DeletedAt { get; set; }
|
|
73
|
+
|
|
74
|
+
// ── Convenience ───────────────────────────────────────────────────────
|
|
75
|
+
/// <summary>Returns <c>true</c> when this record has been soft-deleted.</summary>
|
|
76
|
+
[NotMapped]
|
|
77
|
+
public bool IsDeleted => DeletedAt.HasValue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
// DTOs — kept in the same file for simplicity; move to DTOs/ folder if desired
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
namespace <%= projectName %>.DTOs
|
|
85
|
+
{
|
|
86
|
+
// ── Shared helper ─────────────────────────────────────────────────────────
|
|
87
|
+
using <%= projectName %>.Models;
|
|
88
|
+
|
|
89
|
+
/// <summary>Generic paginated response wrapper.</summary>
|
|
90
|
+
public class PagedResult<T>
|
|
91
|
+
{
|
|
92
|
+
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
|
|
93
|
+
public int Total { get; set; }
|
|
94
|
+
public int Page { get; set; }
|
|
95
|
+
public int PageSize { get; set; }
|
|
96
|
+
public int TotalPages { get; set; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Create DTO ────────────────────────────────────────────────────────────
|
|
100
|
+
/// <summary>Payload for POST api/<%= modelName.toLowerCase() %></summary>
|
|
101
|
+
public class Create<%= modelName %>Dto
|
|
102
|
+
{
|
|
103
|
+
<% (model.fields || []).forEach(field => {
|
|
104
|
+
let csharpType = 'string';
|
|
105
|
+
let defaultValue = '= string.Empty;';
|
|
106
|
+
const n = String(field.name || '').toLowerCase();
|
|
107
|
+
|
|
108
|
+
if (field.type === 'Number') {
|
|
109
|
+
const isDecimal = n.includes('price') || n.includes('amount') || n.includes('total') || n.includes('cost');
|
|
110
|
+
csharpType = isDecimal ? 'decimal' : 'int';
|
|
111
|
+
defaultValue = '';
|
|
112
|
+
} else if (field.type === 'Boolean') {
|
|
113
|
+
csharpType = 'bool'; defaultValue = '= false;';
|
|
114
|
+
} else if (field.type === 'Date' || field.type === 'DateTime') {
|
|
115
|
+
csharpType = 'DateTime?'; defaultValue = '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isEmail = n.includes('email');
|
|
119
|
+
const isUrl = n.includes('url') || n.includes('link');
|
|
120
|
+
const propName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
121
|
+
%>
|
|
122
|
+
<% if (isEmail) { %>[EmailAddress]<% } %>
|
|
123
|
+
<% if (isUrl) { %>[Url]<% } %>
|
|
124
|
+
<% if (csharpType === 'string') { %>[Required][MaxLength(256)]<% } %>
|
|
125
|
+
public <%= csharpType %> <%= propName %> { get; set; } <%= defaultValue %>
|
|
126
|
+
<% }); %>
|
|
127
|
+
|
|
128
|
+
/// <summary>Maps this DTO to a new <see cref="<%= modelName %>"/> entity.</summary>
|
|
129
|
+
public <%= modelName %> ToEntity() => new <%= modelName %>
|
|
130
|
+
{
|
|
131
|
+
<% (model.fields || []).forEach(field => {
|
|
132
|
+
const propName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
133
|
+
%>
|
|
134
|
+
<%= propName %> = this.<%= propName %>,
|
|
135
|
+
<% }); %>
|
|
136
|
+
CreatedAt = DateTime.UtcNow,
|
|
137
|
+
UpdatedAt = DateTime.UtcNow,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Full Update DTO ───────────────────────────────────────────────────────
|
|
142
|
+
/// <summary>Payload for PUT api/<%= modelName.toLowerCase() %>/{id}</summary>
|
|
143
|
+
public class Update<%= modelName %>Dto
|
|
144
|
+
{
|
|
145
|
+
<% (model.fields || []).forEach(field => {
|
|
146
|
+
let csharpType = 'string';
|
|
147
|
+
let defaultValue = '= string.Empty;';
|
|
148
|
+
const n = String(field.name || '').toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (field.type === 'Number') {
|
|
151
|
+
const isDecimal = n.includes('price') || n.includes('amount') || n.includes('total') || n.includes('cost');
|
|
152
|
+
csharpType = isDecimal ? 'decimal' : 'int';
|
|
153
|
+
defaultValue = '';
|
|
154
|
+
} else if (field.type === 'Boolean') {
|
|
155
|
+
csharpType = 'bool'; defaultValue = '= false;';
|
|
156
|
+
} else if (field.type === 'Date' || field.type === 'DateTime') {
|
|
157
|
+
csharpType = 'DateTime?'; defaultValue = '';
|
|
158
|
+
}
|
|
159
|
+
const propName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
160
|
+
%>
|
|
161
|
+
<% if (csharpType === 'string') { %>[Required][MaxLength(256)]<% } %>
|
|
162
|
+
public <%= csharpType %> <%= propName %> { get; set; } <%= defaultValue %>
|
|
163
|
+
<% }); %>
|
|
164
|
+
|
|
165
|
+
/// <summary>Applies all fields from this DTO onto an existing entity.</summary>
|
|
166
|
+
public void ApplyTo(<%= modelName %> entity)
|
|
167
|
+
{
|
|
168
|
+
<% (model.fields || []).forEach(field => {
|
|
169
|
+
const propName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
170
|
+
%>
|
|
171
|
+
entity.<%= propName %> = this.<%= propName %>;
|
|
172
|
+
<% }); %>
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Partial Update DTO ────────────────────────────────────────────────────
|
|
177
|
+
/// <summary>Payload for PATCH api/<%= modelName.toLowerCase() %>/{id} — all fields optional.</summary>
|
|
178
|
+
public class Patch<%= modelName %>Dto
|
|
179
|
+
{
|
|
180
|
+
<% (model.fields || []).forEach(field => {
|
|
181
|
+
let csharpType = 'string?';
|
|
182
|
+
const n = String(field.name || '').toLowerCase();
|
|
183
|
+
|
|
184
|
+
if (field.type === 'Number') {
|
|
185
|
+
const isDecimal = n.includes('price') || n.includes('amount') || n.includes('total') || n.includes('cost');
|
|
186
|
+
csharpType = isDecimal ? 'decimal?' : 'int?';
|
|
187
|
+
} else if (field.type === 'Boolean') {
|
|
188
|
+
csharpType = 'bool?';
|
|
189
|
+
} else if (field.type === 'Date' || field.type === 'DateTime') {
|
|
190
|
+
csharpType = 'DateTime?';
|
|
191
|
+
}
|
|
192
|
+
const propName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
193
|
+
%>
|
|
194
|
+
public <%= csharpType %> <%= propName %> { get; set; }
|
|
195
|
+
<% }); %>
|
|
196
|
+
|
|
197
|
+
/// <summary>Applies only non-null fields from this DTO onto an existing entity.</summary>
|
|
198
|
+
public void ApplyPartialTo(<%= modelName %> entity)
|
|
199
|
+
{
|
|
200
|
+
<% (model.fields || []).forEach(field => {
|
|
201
|
+
const propName = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
202
|
+
const n = String(field.name || '').toLowerCase();
|
|
203
|
+
let csharpType = 'string?';
|
|
204
|
+
if (field.type === 'Number') csharpType = 'numeric?';
|
|
205
|
+
else if (field.type === 'Boolean') csharpType = 'bool?';
|
|
206
|
+
else if (field.type === 'Date' || field.type === 'DateTime') csharpType = 'DateTime?';
|
|
207
|
+
%>
|
|
208
|
+
if (<%= propName %> is not null) entity.<%= propName %> = <%= propName %><% if (csharpType === 'string?') { %><% } %>!;
|
|
209
|
+
<% }); %>
|
|
210
|
+
}
|
|
50
211
|
}
|
|
51
|
-
}
|
|
212
|
+
}
|