-
Notifications
You must be signed in to change notification settings - Fork 37
/
Copy pathDatabaseMigratorExtensions.cs
203 lines (175 loc) · 9.66 KB
/
DatabaseMigratorExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
using Beef.CodeGen;
using DbEx;
using DbEx.DbSchema;
using DbEx.Migration;
using Microsoft.Extensions.Logging;
using OnRamp;
using OnRamp.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Beef.Database
{
/// <summary>
/// Provides extension methods for the <see cref="DatabaseMigrationBase"/>.
/// </summary>
public static class DatabaseMigrationExtensions
{
/// <summary>
/// Performs the database code-generation execution.
/// </summary>
/// <param name="migrator">The <see cref="DatabaseMigrationBase"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns><c>true</c> indicates success; otherwise, <c>false</c>. Additionally, on success the code-generation statistics summary should be returned to append to the log.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Likely to support in the future.")]
public static async Task<(bool Success, string? Statistics)> ExecuteCodeGenAsync(this DatabaseMigrationBase migrator, CancellationToken cancellationToken = default)
{
var margs = (MigrationArgs)migrator.Args;
var cga = new CodeGeneratorArgs();
cga.CopyFrom(margs);
cga.OutputDirectory = margs.OutputDirectory == null ? null : new DirectoryInfo(margs.OutputDirectory.FullName);
cga.Logger = margs.Logger;
cga.ExpectNoChanges = margs.ExpectNoChanges;
cga.IsSimulation = margs.IsSimulation;
cga.AddAssembly(migrator.AdjustArtefactResourceAssemblies());
cga.AddParameters(margs.Parameters);
cga.ValidateCompanyAndAppName();
cga.ScriptFileName = margs.ScriptFileName;
cga.ConfigFileName = margs.ConfigFileName ?? CodeGenFileManager.GetConfigFilename(OnRamp.Console.CodeGenConsole.GetBaseExeDirectory(), CommandType.Database, cga.GetCompany(true), cga.GetAppName(true));
cga.AddDatabaseMigrator(migrator);
migrator.Logger.LogInformation("{Content}", string.Empty);
OnRamp.Console.CodeGenConsole.WriteStandardizedArgs(cga);
try
{
var stats = await CodeGenConsole.ExecuteCodeGenerationAsync(cga).ConfigureAwait(false);
return (true, $", Files: Unchanged = {stats.NotChangedCount}, Updated = {stats.UpdatedCount}, Created = {stats.CreatedCount}, TotalLines = {stats.LinesOfCodeCount}");
}
catch (CodeGenException cgex)
{
migrator.Logger.LogError("{Content}", cgex.Message);
migrator.Logger.LogInformation("{Content}", string.Empty);
return (false, null);
}
catch (CodeGenChangesFoundException cgcfex)
{
migrator.Logger.LogError("{Content}", cgcfex.Message);
migrator.Logger.LogInformation("{Content}", string.Empty);
return (false, null);
}
}
/// <summary>
/// Adjusts the <see cref="DatabaseMigrationBase.ArtefactResourceAssemblies"/> to ensure that the <see cref="MigrationArgs"/>-<see cref="Assembly"/> is included.
/// </summary>
/// <param name="migrator">The <see cref="DatabaseMigrationBase"/>.</param>
/// <returns>The <see cref="DatabaseMigrationBase.ArtefactResourceAssemblies"/> as an array.</returns>
public static Assembly[] AdjustArtefactResourceAssemblies(this DatabaseMigrationBase migrator)
{
var la = (List<Assembly>)migrator.ArtefactResourceAssemblies;
var ma = typeof(MigrationArgs).Assembly;
if (!la.Contains(ma))
{
var ta = migrator.GetType().Assembly;
var i = la.IndexOf(ta);
if (i >= 0)
la.Insert(i + 1, ma);
}
return [.. la];
}
/// <summary>
/// Performs the database entity YAML code-generation execution.
/// </summary>
/// <param name="migrator">The <see cref="DatabaseMigrationBase"/>.</param>
/// <param name="schema">The schema name (where supported).</param>
/// <param name="tables">The table name array.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns><c>true</c> indicates success; otherwise, <c>false</c>. Additionally, on success the code-generation file will be returned.</returns>
public static async Task<(bool Success, string? Statistics)> ExecuteYamlCodeGenAsync(this DatabaseMigrationBase migrator, string? schema, string[] tables, CancellationToken cancellationToken = default)
{
const string templateFileName = "Entity_yaml";
// Infer database schema.
migrator.Logger.LogInformation("{Content}", " ** Code-generation of temporary entity YAML requested **");
migrator.Logger.LogInformation("{Content}", " Querying database to infer table(s)/column(s) schema...");
var dbTables = await migrator.Database.SelectSchemaAsync(migrator, cancellationToken).ConfigureAwait(false);
var data = new DbData();
foreach (var table in tables)
{
var name = CheckTableNameOptions(table, out var includeCrud, out var includeGetByArgs);
var dbTable = dbTables.FirstOrDefault(x => x.Schema == schema && x.Name == name);
if (dbTable is null)
{
migrator.Logger.LogError("{Content}", $"Specified table {migrator.SchemaConfig.ToFullyQualifiedTableName(schema!, name)} not found in database.");
return (false, null);
}
if (!data.Tables.Any(x => x.QualifiedName == dbTable.QualifiedName))
data.Tables.Add(new DbTableSchemaEx(dbTable) { IncludeCrud = includeCrud, IncludeGetByArgs = includeGetByArgs });
}
// Begin code-gen tasks.
migrator.Logger.LogInformation("{Content}", $" Generating YAML for table(s): {string.Join(", ", data.Tables.Select(x => x.QualifiedName))}");
using var sr = StreamLocator.GetTemplateStreamReader(templateFileName, migrator.AdjustArtefactResourceAssemblies(), StreamLocator.HandlebarsExtensions).StreamReader
?? throw new InvalidOperationException($"Embedded Template resource '{templateFileName}' is required and was not found within the selected assemblies.");
// Set the path/filename.
var fn = Path.Combine(OnRamp.Console.CodeGenConsole.GetBaseExeDirectory(), CodeGenFileManager.TemporaryEntityFilename);
var fi = new FileInfo(fn);
// Execute the code-generation proper and write contents (new or overwrite).
await File.WriteAllTextAsync(fi.FullName, new HandlebarsCodeGenerator(sr).Generate(data), cancellationToken).ConfigureAwait(false);
// Done, boom!
migrator.Logger.LogInformation("{Content}", string.Empty);
migrator.Logger.LogWarning("{Content}", $"Temporary entity file created: {fi.FullName}");
migrator.Logger.LogInformation("{Content}", string.Empty);
migrator.Logger.LogInformation("{Content}", "Copy and paste generated contents into their respective files and amend accordingly.");
migrator.Logger.LogInformation("{Content}", $"Once complete it is recommended that the '{fi.Name}' file is deleted, as it is otherwise not used.");
return (true, string.Empty);
}
/// <summary>
/// Check the table name options by checking the first character.
/// </summary>
private static string CheckTableNameOptions(string name, out bool includeCrud, out bool includeGetByArgs)
{
includeCrud = true;
includeGetByArgs = false;
while (true)
{
if (name.Length == 0)
return name;
switch (name[0])
{
case '!':
includeCrud = false;
name = name[1..];
break;
case '*':
includeGetByArgs = true;
name = name[1..];
break;
default:
return name;
}
}
}
/// <summary>
/// Provides the database data contents for code-generation.
/// </summary>
private class DbData
{
public List<DbTableSchemaEx> Tables { get; } = [];
public List<DbTableSchemaEx> RefDataTables => Tables.Where(x => x.IsRefData).ToList();
public List<DbTableSchemaEx> StandardTables => Tables.Where(x => !x.IsRefData).ToList();
}
/// <summary>
/// Extend the <see cref="DbTableSchema"/> to support code-gen requirements.
/// </summary>
private class DbTableSchemaEx(DbTableSchema table) : DbTableSchema(table)
{
public bool IncludeCrud { get; set; }
public bool IncludeGetByArgs { get; set; }
public bool IncludeOperation => IncludeCrud || IncludeGetByArgs;
public string MoustacheDotNetName => "{{" + DotNetName + "}}";
public bool IsPrimaryKeyAnIdentifier => PrimaryKeyColumns.Count == 1 && PrimaryKeyColumns[0].Name.EndsWith(Migration.SchemaConfig.IdColumnNameSuffix);
}
}
}