From f44209e20d3379a9dda6f3b6b780c83616348b26 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 5 Mar 2020 21:33:00 +0800 Subject: Design the data manager interface. --- Timeline/Services/DataManager.cs | 79 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Timeline/Services/DataManager.cs (limited to 'Timeline') diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs new file mode 100644 index 00000000..005ad23c --- /dev/null +++ b/Timeline/Services/DataManager.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Services +{ + public class DataEntry + { +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + public DataInfo Info { get; set; } = default!; + } + + public class DataInfo + { + public DateTime Time { get; set; } + public string Type { get; set; } = default!; + } + + /// + /// A data manager controling data. + /// + /// + /// All data to be saved will be checked identity. + /// Identical data will be saved as one copy and return the same tag. + /// Every data has a ref count. When data is saved, ref count increase. + /// When data is removed, ref count decease. If ref count is decreased + /// to 0, the data entry will be destroyed and no longer occupy space. + /// + /// Type is just an attached attribute for convenience and not participate + /// in identity verification. This should be only used to save blobs but not + /// strings. It will be rare for identity blob with different type, I think. + /// + public interface IDataManager + { + /// + /// Saves the data to a new entry if it does not exist, + /// increases its ref count and returns a tag to the entry. + /// + /// The data. Can't be null. + /// The type of the data. Can't be null. + /// The tag of the created entry. + /// Thrown when or is null. + /// Thrown when a saved copy of data already exists but type is different. + public Task RetainEntry(byte[] data, string type); + + /// + /// Decrease the the ref count of the entry. + /// Remove it if ref count is zero. + /// + /// The tag of the entry. + /// Thrown when is null. + /// + /// It's no-op if entry with tag does not exist. + /// + public Task FreeEntry(string tag); + + /// + /// Retrieve the entry with given tag. + /// + /// The tag of the entry. + /// The entry. + /// Thrown when is null. + /// Thrown when entry with given tag does not exist. + public Task GetEntry(string tag); + + /// + /// Retrieve info of the entry with given tag. + /// + /// The tag of the entry. + /// The entry info. + /// Thrown when is null. + /// Thrown when entry with given tag does not exist. + public Task GetEntryInfo(string tag); + } + +} -- cgit v1.2.3 From 57b29adef10dcefcacf255df05cf6bc11512caf2 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 5 Mar 2020 23:02:22 +0800 Subject: Implement data manager. --- Timeline/Entities/DataEntity.cs | 23 +++++++ Timeline/Entities/DatabaseContext.cs | 1 + Timeline/Services/DataManager.cs | 122 ++++++++++++++++++++++++----------- 3 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 Timeline/Entities/DataEntity.cs (limited to 'Timeline') diff --git a/Timeline/Entities/DataEntity.cs b/Timeline/Entities/DataEntity.cs new file mode 100644 index 00000000..b21e2dbf --- /dev/null +++ b/Timeline/Entities/DataEntity.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("data")] + public class DataEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("tag"), Required] + public string Tag { get; set; } = default!; + + [Column("data"), Required] +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + + [Column("ref"), Required] + public int Ref { get; set; } + } +} diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 039cbd51..3ed61b71 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -21,5 +21,6 @@ namespace Timeline.Entities public DbSet TimelinePosts { get; set; } = default!; public DbSet TimelineMembers { get; set; } = default!; public DbSet JwtToken { get; set; } = default!; + public DbSet Data { get; set; } = default!; } } diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs index 005ad23c..66aa6f81 100644 --- a/Timeline/Services/DataManager.cs +++ b/Timeline/Services/DataManager.cs @@ -1,37 +1,19 @@ -using System; -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using System; using System.Linq; using System.Threading.Tasks; +using Timeline.Entities; namespace Timeline.Services { - public class DataEntry - { -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - public DataInfo Info { get; set; } = default!; - } - - public class DataInfo - { - public DateTime Time { get; set; } - public string Type { get; set; } = default!; - } - /// - /// A data manager controling data. + /// A data manager controlling data. /// /// - /// All data to be saved will be checked identity. /// Identical data will be saved as one copy and return the same tag. - /// Every data has a ref count. When data is saved, ref count increase. - /// When data is removed, ref count decease. If ref count is decreased + /// Every data has a ref count. When data is retained, ref count increase. + /// When data is freed, ref count decease. If ref count is decreased /// to 0, the data entry will be destroyed and no longer occupy space. - /// - /// Type is just an attached attribute for convenience and not participate - /// in identity verification. This should be only used to save blobs but not - /// strings. It will be rare for identity blob with different type, I think. /// public interface IDataManager { @@ -40,11 +22,9 @@ namespace Timeline.Services /// increases its ref count and returns a tag to the entry. /// /// The data. Can't be null. - /// The type of the data. Can't be null. /// The tag of the created entry. - /// Thrown when or is null. - /// Thrown when a saved copy of data already exists but type is different. - public Task RetainEntry(byte[] data, string type); + /// Thrown when is null. + public Task RetainEntry(byte[] data); /// /// Decrease the the ref count of the entry. @@ -61,19 +41,83 @@ namespace Timeline.Services /// Retrieve the entry with given tag. /// /// The tag of the entry. - /// The entry. + /// The data of the entry. /// Thrown when is null. /// Thrown when entry with given tag does not exist. - public Task GetEntry(string tag); - - /// - /// Retrieve info of the entry with given tag. - /// - /// The tag of the entry. - /// The entry info. - /// Thrown when is null. - /// Thrown when entry with given tag does not exist. - public Task GetEntryInfo(string tag); + public Task GetEntry(string tag); } + public class DataManager : IDataManager + { + private readonly DatabaseContext _database; + private readonly IETagGenerator _eTagGenerator; + + public DataManager(DatabaseContext database, IETagGenerator eTagGenerator) + { + _database = database; + _eTagGenerator = eTagGenerator; + } + + public async Task RetainEntry(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var tag = await _eTagGenerator.Generate(data); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity == null) + { + entity = new DataEntity + { + Tag = tag, + Data = data, + Ref = 1 + }; + _database.Data.Add(entity); + await _database.SaveChangesAsync(); + } + else + { + entity.Ref += 1; + await _database.SaveChangesAsync(); + } + return tag; + } + + public async Task FreeEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity != null) + { + if (entity.Ref == 1) + { + _database.Data.Remove(entity); + } + else + { + entity.Ref -= 1; + } + await _database.SaveChangesAsync(); + } + } + + public async Task GetEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); + + if (entity == null) + throw new InvalidOperationException("Entry with given tag does not exist."); + + return entity.Data; + } + } } -- cgit v1.2.3 From a88ce6495185e7e63c5c1362c7e3f8660b89333d Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 5 Mar 2020 23:27:41 +0800 Subject: Migrate avatar service. TODO: Migrate database. --- Timeline/Entities/UserAvatarEntity.cs | 7 +- .../Resources/Services/DataManager.Designer.cs | 72 ++++++++++++ Timeline/Resources/Services/DataManager.resx | 123 +++++++++++++++++++++ Timeline/Services/DataManager.cs | 2 +- Timeline/Services/UserAvatarService.cs | 43 +++---- Timeline/Timeline.csproj | 9 ++ 6 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 Timeline/Resources/Services/DataManager.Designer.cs create mode 100644 Timeline/Resources/Services/DataManager.resx (limited to 'Timeline') diff --git a/Timeline/Entities/UserAvatarEntity.cs b/Timeline/Entities/UserAvatarEntity.cs index be094a77..3c2720f7 100644 --- a/Timeline/Entities/UserAvatarEntity.cs +++ b/Timeline/Entities/UserAvatarEntity.cs @@ -11,15 +11,12 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } - [Column("data")] - public byte[]? Data { get; set; } + [Column("data_tag")] + public string? DataTag { get; set; } [Column("type")] public string? Type { get; set; } - [Column("etag")] - public string? ETag { get; set; } - [Column("last_modified"), Required] public DateTime LastModified { get; set; } diff --git a/Timeline/Resources/Services/DataManager.Designer.cs b/Timeline/Resources/Services/DataManager.Designer.cs new file mode 100644 index 00000000..0872059a --- /dev/null +++ b/Timeline/Resources/Services/DataManager.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DataManager { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal DataManager() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.DataManager", typeof(DataManager).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Entry with given tag does not exist.. + /// + internal static string ExceptionEntryNotExist { + get { + return ResourceManager.GetString("ExceptionEntryNotExist", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/DataManager.resx b/Timeline/Resources/Services/DataManager.resx new file mode 100644 index 00000000..688e0e96 --- /dev/null +++ b/Timeline/Resources/Services/DataManager.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Entry with given tag does not exist. + + \ No newline at end of file diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs index 66aa6f81..d6b8b6a4 100644 --- a/Timeline/Services/DataManager.cs +++ b/Timeline/Services/DataManager.cs @@ -115,7 +115,7 @@ namespace Timeline.Services var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); if (entity == null) - throw new InvalidOperationException("Entry with given tag does not exist."); + throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist); return entity.Data; } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 39b408e6..5a07da96 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -164,7 +164,7 @@ namespace Timeline.Services private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; private readonly IUserAvatarValidator _avatarValidator; - private readonly IETagGenerator _eTagGenerator; + private readonly IDataManager _dataManager; private readonly IClock _clock; @@ -173,20 +173,20 @@ namespace Timeline.Services DatabaseContext database, IDefaultUserAvatarProvider defaultUserAvatarProvider, IUserAvatarValidator avatarValidator, - IETagGenerator eTagGenerator, + IDataManager dataManager, IClock clock) { _logger = logger; _database = database; _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; - _eTagGenerator = eTagGenerator; + _dataManager = dataManager; _clock = clock; } public async Task GetAvatarETag(long id) { - var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; + var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; if (eTag == null) return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); else @@ -195,25 +195,27 @@ namespace Timeline.Services public async Task GetAvatar(long id) { - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); if (avatarEntity != null) { - if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null)) + if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) { var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; _logger.LogCritical(message); throw new DatabaseCorruptedException(message); } - if (avatarEntity.Data != null) + + if (avatarEntity.DataTag != null) { + var data = await _dataManager.GetEntry(avatarEntity.DataTag); return new AvatarInfo { Avatar = new Avatar { Type = avatarEntity.Type!, - Data = avatarEntity.Data + Data = data }, LastModified = avatarEntity.LastModified }; @@ -239,15 +241,15 @@ namespace Timeline.Services if (avatar == null) { - if (avatarEntity == null || avatarEntity.Data == null) + if (avatarEntity == null || avatarEntity.DataTag == null) { return; } else { - avatarEntity.Data = null; + await _dataManager.FreeEntry(avatarEntity.DataTag); + avatarEntity.DataTag = null; avatarEntity.Type = null; - avatarEntity.ETag = null; avatarEntity.LastModified = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); @@ -256,24 +258,26 @@ namespace Timeline.Services else { await _avatarValidator.Validate(avatar); + var oldTag = avatarEntity?.DataTag; var create = avatarEntity == null; - if (create) + if (avatarEntity == null) { avatarEntity = new UserAvatarEntity(); + _database.UserAvatars.Add(avatarEntity); } - avatarEntity!.Type = avatar.Type; - avatarEntity.Data = avatar.Data; - avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data); + var tag = await _dataManager.RetainEntry(avatar.Data); + avatarEntity.DataTag = tag; + avatarEntity.Type = avatar.Type; avatarEntity.LastModified = _clock.GetCurrentTime(); avatarEntity.UserId = id; - if (create) - { - _database.UserAvatars.Add(avatarEntity); - } await _database.SaveChangesAsync(); _logger.LogInformation(create ? Resources.Services.UserAvatarService.LogCreateEntity : Resources.Services.UserAvatarService.LogUpdateEntity); + if (oldTag != null) + { + await _dataManager.FreeEntry(oldTag); + } } } } @@ -282,7 +286,6 @@ namespace Timeline.Services { public static void AddUserAvatarService(this IServiceCollection services) { - services.TryAddTransient(); services.AddScoped(); services.AddSingleton(); services.AddTransient(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 2dcc3c9f..72bc1572 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -97,6 +97,11 @@ True Validator.resx + + True + True + DataManager.resx + True True @@ -174,6 +179,10 @@ ResXFileCodeGenerator Validator.Designer.cs + + ResXFileCodeGenerator + DataManager.Designer.cs + ResXFileCodeGenerator Exception.Designer.cs -- cgit v1.2.3 From 81218733d494452e04a1ab84fae242d0bba08092 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 6 Mar 2020 18:12:51 +0800 Subject: ... --- Timeline/Services/DataManager.cs | 3 +-- Timeline/Services/UserAvatarService.cs | 4 ++-- Timeline/Startup.cs | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) (limited to 'Timeline') diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs index d6b8b6a4..d447b0d5 100644 --- a/Timeline/Services/DataManager.cs +++ b/Timeline/Services/DataManager.cs @@ -76,13 +76,12 @@ namespace Timeline.Services Ref = 1 }; _database.Data.Add(entity); - await _database.SaveChangesAsync(); } else { entity.Ref += 1; - await _database.SaveChangesAsync(); } + await _database.SaveChangesAsync(); return tag; } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 5a07da96..52d079a3 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -258,6 +258,7 @@ namespace Timeline.Services else { await _avatarValidator.Validate(avatar); + var tag = await _dataManager.RetainEntry(avatar.Data); var oldTag = avatarEntity?.DataTag; var create = avatarEntity == null; if (avatarEntity == null) @@ -265,7 +266,6 @@ namespace Timeline.Services avatarEntity = new UserAvatarEntity(); _database.UserAvatars.Add(avatarEntity); } - var tag = await _dataManager.RetainEntry(avatar.Data); avatarEntity.DataTag = tag; avatarEntity.Type = avatar.Type; avatarEntity.LastModified = _clock.GetCurrentTime(); @@ -287,7 +287,7 @@ namespace Timeline.Services public static void AddUserAvatarService(this IServiceCollection services) { services.AddScoped(); - services.AddSingleton(); + services.AddScoped(); services.AddTransient(); } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 14bd14cc..263e6b7a 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -93,6 +93,10 @@ namespace Timeline services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddUserAvatarService(); services.AddScoped(); -- cgit v1.2.3 From cc57bddf48fee422850c8b458ac9a22ba0bfaa64 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 6 Mar 2020 19:16:58 +0800 Subject: Migrate database. --- Timeline/Entities/DatabaseContext.cs | 1 + .../20200306110049_AddDataTable.Designer.cs | 290 +++++++++++++++++++++ Timeline/Migrations/20200306110049_AddDataTable.cs | 87 +++++++ .../20200306111553_DropUserDetails.Designer.cs | 290 +++++++++++++++++++++ .../Migrations/20200306111553_DropUserDetails.cs | 17 ++ .../Migrations/DatabaseContextModelSnapshot.cs | 37 ++- Timeline/Timeline.csproj | 4 + 7 files changed, 720 insertions(+), 6 deletions(-) create mode 100644 Timeline/Migrations/20200306110049_AddDataTable.Designer.cs create mode 100644 Timeline/Migrations/20200306110049_AddDataTable.cs create mode 100644 Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs create mode 100644 Timeline/Migrations/20200306111553_DropUserDetails.cs (limited to 'Timeline') diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 3ed61b71..8899308c 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -13,6 +13,7 @@ namespace Timeline.Entities { modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); modelBuilder.Entity().HasIndex(e => e.Username).IsUnique(); + modelBuilder.Entity().HasIndex(e => e.Tag).IsUnique(); } public DbSet Users { get; set; } = default!; diff --git a/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs b/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs new file mode 100644 index 00000000..336ffc18 --- /dev/null +++ b/Timeline/Migrations/20200306110049_AddDataTable.Designer.cs @@ -0,0 +1,290 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200306110049_AddDataTable")] + partial class AddDataTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20200306110049_AddDataTable.cs b/Timeline/Migrations/20200306110049_AddDataTable.cs new file mode 100644 index 00000000..e33bf4c9 --- /dev/null +++ b/Timeline/Migrations/20200306110049_AddDataTable.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddDataTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "data", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + tag = table.Column(nullable: false), + data = table.Column(nullable: false), + @ref = table.Column(name: "ref", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_data", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "IX_data_tag", + table: "data", + column: "tag", + unique: true); + + migrationBuilder.Sql(@" +ALTER TABLE user_avatars + RENAME TO user_avatars_backup; + +CREATE TABLE user_avatars ( + id INTEGER NOT NULL + CONSTRAINT PK_user_avatars PRIMARY KEY AUTOINCREMENT, + data_tag TEXT, + type TEXT, + last_modified TEXT NOT NULL, + user INTEGER NOT NULL, + CONSTRAINT FK_user_avatars_users_user FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO user_avatars (id, data_tag, type, last_modified, user) + SELECT id, etag, type, last_modified, user FROM user_avatars_backup; + +INSERT OR IGNORE INTO data (tag, data, ref) + SELECT etag, data, 0 FROM user_avatars_backup; + +UPDATE data +SET ref = (SELECT COUNT (*) + FROM user_avatars_backup AS a + WHERE a.etag == data.tag); + +DROP TABLE user_avatars_backup; + +CREATE UNIQUE INDEX IX_user_avatars_user ON user_avatars (user); + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "data"); + + migrationBuilder.DropColumn( + name: "data_tag", + table: "user_avatars"); + + migrationBuilder.AddColumn( + name: "data", + table: "user_avatars", + type: "BLOB", + nullable: true); + + migrationBuilder.AddColumn( + name: "etag", + table: "user_avatars", + type: "TEXT", + nullable: true); + } + } +} diff --git a/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs b/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs new file mode 100644 index 00000000..f0c4dc08 --- /dev/null +++ b/Timeline/Migrations/20200306111553_DropUserDetails.Designer.cs @@ -0,0 +1,290 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20200306111553_DropUserDetails")] + partial class DropUserDetails + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20200306111553_DropUserDetails.cs b/Timeline/Migrations/20200306111553_DropUserDetails.cs new file mode 100644 index 00000000..0a176461 --- /dev/null +++ b/Timeline/Migrations/20200306111553_DropUserDetails.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class DropUserDetails : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "user_details"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index c4b00f47..8170b2f0 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -16,6 +16,35 @@ namespace Timeline.Migrations modelBuilder .HasAnnotation("ProductVersion", "3.1.2"); + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => { b.Property("Id") @@ -142,12 +171,8 @@ namespace Timeline.Migrations .HasColumnName("id") .HasColumnType("INTEGER"); - b.Property("Data") - .HasColumnName("data") - .HasColumnType("BLOB"); - - b.Property("ETag") - .HasColumnName("etag") + b.Property("DataTag") + .HasColumnName("data_tag") .HasColumnType("TEXT"); b.Property("LastModified") diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 72bc1572..e993c0b3 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -7,6 +7,10 @@ 8.0 enable + + + + -- cgit v1.2.3