| | | 1 | | #region Usings |
| | | 2 | | using ZhEaIsNsAaBn.DbSeeder.Registrations; |
| | | 3 | | #endregion |
| | | 4 | | |
| | | 5 | | namespace ZhEaIsNsAaBn.DbSeeder.Design; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// The DbSeeder design-time commands, hosted by your own console project (e.g. a migrations CLI). Development only: the |
| | | 9 | | /// database and write files under the project folder. |
| | | 10 | | /// <list type="bullet"> |
| | | 11 | | /// <item><c>profile list | add <name> -t <table>… [-o folder] | edit <name> | remove <name></c> |
| | | 12 | | /// <item><c>baseline [-p profile] [-n name]</c> — the first migration of an existing database</item> |
| | | 13 | | /// <item><c>add <name> [-p profile]</c> — a migration from the changes since <c>.snapshot/</c></item> |
| | | 14 | | /// <item><c>snapshot rebuild [-p profile]</c> — regenerate <c>.snapshot/</c> from the database</item> |
| | | 15 | | /// <item><c>validate [-f folder]</c> — every data file against the model; exit code 1 on errors (for CI)</item> |
| | | 16 | | /// <item><c>list</c>, <c>update [--to id]</c> — status and apply, from the migrations folder on disk</item> |
| | | 17 | | /// <item><c>down [--steps n | --to id] [--soft | --hard] [-y]</c> — plan, confirm, roll back; <c>--force</c> = dry run |
| | | 18 | | /// tree; <c>--force --execute</c> = delete it (never in Production or a protected database; the database name must be t |
| | | 19 | | /// <item><c>script [--from id] [--to id] [-o file]</c> — idempotent SQL for a DBA</item> |
| | | 20 | | /// </list> |
| | | 21 | | /// Every command takes <c>-d|--dir</c> (project folder; default the current directory). |
| | | 22 | | /// </summary> |
| | | 23 | | public static class DbSeederCli |
| | | 24 | | { |
| | | 25 | | /// <summary>Runs one command on <typeparamref name="TDbContext"/>.</summary> |
| | | 26 | | /// <typeparam name="TDbContext">The context; it needs an <c>AddDbSeeder<TDbContext>()…Build()</c> registratio |
| | | 27 | | /// <param name="args">The command line, e.g. <c>[ "add", "AddWallet" ]</c>.</param> |
| | | 28 | | /// <param name="services">The app's services (the context and its seed registration).</param> |
| | | 29 | | /// <param name="cancellationToken">Token to cancel the command.</param> |
| | | 30 | | /// <returns>The exit code: 0 on success, 1 on errors (Spectre's own code for a bad command line).</returns> |
| | | 31 | | /// <remarks>Prompts only when the console's profile is interactive (<c>Profile.Capabilities.Interactive</c>). Spect |
| | | 32 | | /// makes a redirected input or a CI server non-interactive: there <c>down</c> needs <c>--yes</c> and <c>--force --e |
| | | 33 | | /// <exception cref="ArgumentNullException"><paramref name="args"/> or <paramref name="services"/> is <see langword= |
| | | 34 | | /// <exception cref="InvalidOperationException">The context or its seed registration isn't registered.</exception> |
| | | 35 | | /// <example><code>return await DbSeederCli.RunAsync<AppDbContext>( args, host.Services );</code></example> |
| | | 36 | | public static Task<int> RunAsync< TDbContext >( string[] args, IServiceProvider services, CancellationToken cancella |
| | | 37 | | where TDbContext : DbContext |
| | 0 | 38 | | => RunAsync<TDbContext>( args, services, AnsiConsole.Console, cancellationToken ); |
| | | 39 | | |
| | | 40 | | |
| | | 41 | | internal static Task<int> RunAsync< TDbContext >( string[] args, IServiceProvider services, IAnsiConsole console, Ca |
| | | 42 | | where TDbContext : DbContext |
| | 411 | 43 | | => RunWithContextAsync<TDbContext>( args, services, console, null, cancellationToken ); |
| | | 44 | | |
| | | 45 | | /// The same, on <paramref name="factoryContext"/> when given (from an <c>IDesignTimeDbContextFactory</c>; disposed |
| | | 46 | | /// services' context. Called by <see cref="DbSeederDesignTime"/> through reflection (the context type is only known |
| | | 47 | | internal static async Task<int> RunWithContextAsync< TDbContext >( string[] args, IServiceProvider services, IAnsiCo |
| | | 48 | | CancellationToken cancellationToken ) |
| | | 49 | | where TDbContext : DbContext |
| | | 50 | | { |
| | 411 | 51 | | ArgumentNullException.ThrowIfNull( args ); |
| | 411 | 52 | | ArgumentNullException.ThrowIfNull( services ); |
| | | 53 | | |
| | 411 | 54 | | var scope = services.CreateAsyncScope(); |
| | | 55 | | |
| | 411 | 56 | | await using var scopeDisposal = scope.ConfigureAwait( false ); |
| | | 57 | | |
| | 411 | 58 | | var registration = scope.ServiceProvider.GetServices<SeedContextRegistration>().FirstOrDefault( r => r.ContextTy |
| | 411 | 59 | | ?? throw new InvalidOperationException( $"{typeof( TDbContext ).Name} has no AddDbSeeder<{typeof |
| | | 60 | | |
| | 411 | 61 | | var (context, owned) = factoryContext is TDbContext created ? ( created, true ) : SeedContextResolver.Resolve<TD |
| | | 62 | | |
| | | 63 | | try |
| | | 64 | | { |
| | 411 | 65 | | var app = new CommandApp( new DesignTypeRegistrar( new DesignSession( context, registration.Configuration, s |
| | | 66 | | |
| | 411 | 67 | | app.Configure( config => |
| | 411 | 68 | | { |
| | 411 | 69 | | config.SetApplicationName( "dbseeder" ); |
| | 411 | 70 | | config.ConfigureConsole( console ); |
| | 411 | 71 | | |
| | 411 | 72 | | config.AddBranch( "profile", profile => |
| | 411 | 73 | | { |
| | 411 | 74 | | profile.SetDescription( "Capture profiles (.dbseeder/profiles.json)" ); |
| | 411 | 75 | | profile.AddCommand<ProfileListCommand>( "list" ).WithDescription( "List the profiles" ); |
| | 411 | 76 | | profile.AddCommand<ProfileAddCommand>( "add" ).WithDescription( "Add a profile" ); |
| | 411 | 77 | | profile.AddCommand<ProfileEditCommand>( "edit" ).WithDescription( "Change a profile's tables or fold |
| | 411 | 78 | | profile.AddCommand<ProfileRemoveCommand>( "remove" ).WithDescription( "Remove a profile" ); |
| | 411 | 79 | | } ); |
| | 411 | 80 | | config.AddCommand<BaselineCommand>( "baseline" ).WithDescription( "First migration for an existing datab |
| | 411 | 81 | | config.AddCommand<AddCommand>( "add" ).WithDescription( "Migration from the changes since the snapshot" |
| | 411 | 82 | | config.AddBranch( "snapshot", snapshot => snapshot.AddCommand<SnapshotRebuildCommand>( "rebuild" ).WithD |
| | 411 | 83 | | config.AddCommand<ValidateCommand>( "validate" ).WithDescription( "Check every data file against the mod |
| | 411 | 84 | | config.AddCommand<ListCommand>( "list" ).WithDescription( "Applied / pending seed migrations, checksums, |
| | 411 | 85 | | config.AddCommand<UpdateCommand>( "update" ).WithDescription( "Apply the pending seed migrations (--to < |
| | 411 | 86 | | config.AddCommand<DownCommand>( "down" ).WithDescription( "Roll back (--steps n | --to <id>) [--soft | - |
| | 411 | 87 | | config.AddCommand<ScriptCommand>( "script" ).WithDescription( "Idempotent SQL script for a DBA (--from, |
| | 411 | 88 | | } ); |
| | | 89 | | |
| | 411 | 90 | | return await app.RunAsync( args, cancellationToken ).ConfigureAwait( false ); |
| | | 91 | | } |
| | | 92 | | finally |
| | | 93 | | { |
| | 411 | 94 | | if ( owned ) { await context.DisposeAsync().ConfigureAwait( false ); } |
| | | 95 | | } |
| | 411 | 96 | | } |
| | | 97 | | } |