| | | 1 | | #region Usings |
| | | 2 | | using System.Text.Json.Serialization; |
| | | 3 | | using System.Text.RegularExpressions; |
| | | 4 | | #endregion |
| | | 5 | | |
| | | 6 | | namespace ZhEaIsNsAaBn.DbSeeder; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Reads the seed migrations embedded in an assembly by <c>buildTransitive/ZhEaIsNsAaBn.DbSeeder.props</c>: resources n |
| | | 10 | | /// <c>{folder}/{id}/manifest.json</c> and <c>{folder}/{id}/{Table}.json</c> (separators normalised to <c>/</c>). |
| | | 11 | | /// </summary> |
| | | 12 | | internal static partial class SeedMigrationSource |
| | | 13 | | { |
| | 10 | 14 | | internal static readonly JsonSerializerOptions JsonOptions = new() |
| | 10 | 15 | | { |
| | 10 | 16 | | UnmappedMemberHandling = JsonUnmappedMemberHandling. |
| | 10 | 17 | | ReadCommentHandling = JsonCommentHandling.Disallo |
| | 10 | 18 | | AllowTrailingCommas = false |
| | 10 | 19 | | }; |
| | | 20 | | |
| | | 21 | | |
| | | 22 | | /// Reads every embedded file under <paramref name="folder"/> and parses the migrations of <paramref name="contextTy |
| | | 23 | | internal static LayerResponse<IReadOnlyList<SeedMigrationFiles>> FromAssembly( Assembly assembly, string folder, str |
| | | 24 | | { |
| | 131 | 25 | | var files = new Dictionary<string, string>( StringComparer.Ordinal ); |
| | 131 | 26 | | var prefix = NormalizeFolder( folder ) + "/"; |
| | | 27 | | |
| | | 28 | | |
| | 3384 | 29 | | foreach ( var name in assembly.GetManifestResourceNames() ) |
| | | 30 | | { |
| | 1561 | 31 | | var path = name.Replace( '\\', '/' ); |
| | | 32 | | |
| | 1561 | 33 | | if ( !path.StartsWith( prefix, StringComparison.Ordinal ) || !path.EndsWith( ".json", StringComparison.Ordin |
| | | 34 | | |
| | | 35 | | |
| | 1561 | 36 | | using var stream = assembly.GetManifestResourceStream( name ) ?? throw new InvalidOperationException( $"Reso |
| | 1561 | 37 | | using var reader = new StreamReader( stream ); |
| | | 38 | | |
| | 1561 | 39 | | files[ path ] = reader.ReadToEnd(); |
| | | 40 | | } |
| | | 41 | | |
| | | 42 | | |
| | 131 | 43 | | return Parse( files, folder, contextType ); |
| | | 44 | | } |
| | | 45 | | |
| | | 46 | | /// Parses the migrations of <paramref name="contextType"/> from <paramref name="files"/> (resource path → text), in |
| | | 47 | | internal static LayerResponse<IReadOnlyList<SeedMigrationFiles>> Parse( IReadOnlyDictionary<string, string> files, s |
| | | 48 | | { |
| | 581 | 49 | | var prefix = NormalizeFolder( folder ) + "/"; |
| | 581 | 50 | | var migrations = new List<SeedMigrationFiles>(); |
| | | 51 | | |
| | | 52 | | |
| | 3182 | 53 | | foreach ( var manifestPath in files.Keys.Where( p => p.StartsWith( prefix, StringComparison.Ordinal ) && p.EndsW |
| | 581 | 54 | | .Order( StringComparer.Ordinal ) ) |
| | | 55 | | { |
| | 1025 | 56 | | var folderName = manifestPath[ prefix.Length..^( SeedChecksum.ManifestFile.Length + 1 ) ]; |
| | | 57 | | |
| | 1025 | 58 | | if ( folderName.Contains( '/', StringComparison.Ordinal ) ) { continue; } // only {folder}/{id}/manifest.j |
| | | 59 | | |
| | | 60 | | |
| | 1022 | 61 | | var parsed = Deserialize<SeedManifest>( manifestPath, files[ manifestPath ] ); |
| | | 62 | | |
| | 1022 | 63 | | if ( parsed.IsFailure ) { return LayerResponse<IReadOnlyList<SeedMigrationFiles>>.Failure( parsed ); } |
| | | 64 | | |
| | | 65 | | |
| | 1022 | 66 | | var manifest = parsed.Result!; |
| | | 67 | | |
| | 1022 | 68 | | if ( !string.Equals( manifest.ContextType, contextType, StringComparison.Ordinal ) ) { continue; } |
| | | 69 | | |
| | 832 | 70 | | if ( !IdPattern().IsMatch( manifest.Id ) ) |
| | | 71 | | { |
| | 3 | 72 | | return SeedErrors.InvalidDataFile( manifestPath, $"Id '{manifest.Id}' isn't yyyyMMddHHmmss_Name." ); |
| | | 73 | | } |
| | | 74 | | |
| | 829 | 75 | | if ( manifest.Id != folderName ) |
| | | 76 | | { |
| | 3 | 77 | | return SeedErrors.InvalidDataFile( manifestPath, $"Id '{manifest.Id}' doesn't match the folder '{folderN |
| | | 78 | | } |
| | | 79 | | |
| | | 80 | | |
| | 826 | 81 | | var tables = new List<(string File, string Path, SeedTableFile Table)>(); |
| | 826 | 82 | | var checksums = new List<KeyValuePair<string, string>> { new( SeedChecksum.ManifestFile, files[ manifestPath |
| | | 83 | | |
| | 5860 | 84 | | foreach ( var file in manifest.Tables ) |
| | | 85 | | { |
| | 2116 | 86 | | if ( file.Contains( '/', StringComparison.Ordinal ) || file.Contains( '\\', StringComparison.Ordinal ) | |
| | 2116 | 87 | | || tables.Any( t => t.File == file ) ) |
| | | 88 | | { |
| | 6 | 89 | | return SeedErrors.InvalidDataFile( manifestPath, $"Table file '{file}' must be a distinct file name |
| | | 90 | | } |
| | | 91 | | |
| | 2110 | 92 | | var path = $"{prefix}{folderName}/{file}"; |
| | | 93 | | |
| | 2113 | 94 | | if ( !files.TryGetValue( path, out var text ) ) { return SeedErrors.MigrationNotFound( manifest.Id, file |
| | | 95 | | |
| | | 96 | | |
| | 2107 | 97 | | var table = Deserialize<SeedTableFile>( path, text ); |
| | | 98 | | |
| | 2122 | 99 | | if ( table.IsFailure ) { return LayerResponse<IReadOnlyList<SeedMigrationFiles>>.Failure( table ); } |
| | | 100 | | |
| | 2092 | 101 | | tables.Add( ( file, path, table.Result! ) ); |
| | 2092 | 102 | | checksums.Add( new KeyValuePair<string, string>( file, text ) ); |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | |
| | 802 | 106 | | migrations.Add( new SeedMigrationFiles { Id = manifest.Id, Manifest = manifest, Tables = tables, Checksum = |
| | | 107 | | } |
| | | 108 | | |
| | | 109 | | |
| | 551 | 110 | | return migrations; |
| | 30 | 111 | | } |
| | | 112 | | |
| | | 113 | | private static LayerResponse<T> Deserialize< T >( string path, string text ) |
| | | 114 | | where T : class |
| | | 115 | | { |
| | | 116 | | try |
| | | 117 | | { |
| | 3129 | 118 | | return JsonSerializer.Deserialize<T>( SeedChecksum.Normalize( text ), JsonOptions ) is { } value |
| | 3129 | 119 | | ? value |
| | 3129 | 120 | | : SeedErrors.InvalidDataFile( path, "The file is empty (null)." ); |
| | | 121 | | } |
| | 24 | 122 | | catch ( JsonException ex ) { return SeedErrors.InvalidDataFile( path, ex.Message, ex ); } |
| | 3129 | 123 | | } |
| | | 124 | | |
| | 712 | 125 | | private static string NormalizeFolder( string folder ) => folder.Replace( '\\', '/' ).Trim( '/' ); |
| | | 126 | | |
| | | 127 | | [ GeneratedRegex( "^[0-9]{14}_[A-Za-z][A-Za-z0-9_]*$", RegexOptions.CultureInvariant ) ] |
| | | 128 | | private static partial Regex IdPattern(); |
| | | 129 | | } |