diff --git a/Controllers/AutoChildController.cs b/Controllers/AutoChildController.cs new file mode 100644 index 0000000..7b28f96 --- /dev/null +++ b/Controllers/AutoChildController.cs @@ -0,0 +1,103 @@ +using System.Linq.Expressions; +using System.Reflection; +using ApiSourceGeneratorHelper; +using AutoMapPropertyHelper; +using EntityFrameworkCore.Projectables; +using EntityFrameworkCore.Projectables.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NejCommon.Utils; +using Swashbuckle.AspNetCore.Annotations; +using NejCommon.Models; + +namespace NejCommon.Controllers +{ + public abstract class AutoChildController : AutoChildController + where TType : class, new() + where TRequest : IAutomappedAttribute, new() + where TResponse : IAutomappedAttribute, new() + { + public AutoChildController(CommonDbContext appDb, IServiceProvider providers) : base(appDb, providers) + { + } + } + + /// + /// + /// + /// The underyling type + /// The request type + /// The response type + [ApiController] + public abstract class AutoChildController : ControllerBase + where TType : class, new() + where TGetResponse : IAutomappedAttribute, new() + where TUpdateRequest : IAutomappedAttribute, new() + where TUpdateResponse : IAutomappedAttribute, new() + { + protected readonly CommonDbContext db; + protected readonly IServiceProvider providers; + + public AutoChildController(CommonDbContext appDb, IServiceProvider providers) : base() + { + db = appDb; + this.providers = providers; + } + + protected abstract TType GetQuery(TOwner comp); + protected abstract void Assign(TOwner comp, TType query); + public virtual bool Trackable => true; + + /// + /// Gets the + /// + /// + /// Success + /// There was an error + [HttpGet] + [Route("")] + public virtual async Task>> Get([FromServices] TOwner owner) + { + var entity = GetQuery(owner); + var dat = new TGetResponse().ApplyFrom(providers, entity); + return TypedResults.Ok(dat); + } + + /// + /// Update company. + /// + /// + /// + /// Success + /// There was an error + [HttpPut] + [Route("")] + public virtual async Task, Ok>> Update([FromServices] TOwner owner, [FromBody] TUpdateRequest body) + { + var entity = GetQuery(owner); + if (entity == null) + { + if (Trackable) + entity = db.Create(); + else + entity = new TType(); + Assign(owner, entity); + } + /* + if(entity is InvoiceBase inv){ + //Console.Writeline(inv.InvoiceItems.Count); + }*/ + body.ApplyTo(providers, entity); + + var dat = new TUpdateResponse().ApplyFrom(providers, entity); + + var res = await db.ApiSaveChangesAsyncOk(providers, entity); + + //use the private constructor thru reflection + var ctor = typeof(Results, Ok>).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0]; + return (Results, Ok>)ctor.Invoke(new object[] { res.Result }); + } + } +} diff --git a/Controllers/AutoController.cs b/Controllers/AutoController.cs index c4f9e7a..5d2945c 100644 --- a/Controllers/AutoController.cs +++ b/Controllers/AutoController.cs @@ -8,26 +8,20 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using NejAccountingAPI.Controllers; -using NejAccountingAPI.Models; using NejCommon.Utils; using Swashbuckle.AspNetCore.Annotations; +using NejCommon.Models; +using System.Runtime.CompilerServices; namespace NejCommon.Controllers { - public abstract class AutoController : AutoController where TType : class, new() where TRequest : IAutomappedAttribute, new() where TResponse : IAutomappedAttribute, new() - { - public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers) - { - } - } public abstract class AutoController : AutoController where TType : class, new() where TRequest : IAutomappedAttribute, new() where TResponse : IAutomappedAttribute, new() { - public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers) + public AutoController(CommonDbContext appDb, IServiceProvider providers) : base(appDb, providers) { } } @@ -39,7 +33,7 @@ namespace NejCommon.Controllers /// The request type /// The response type [ApiController] - public abstract class AutoController : ControllerBase + public abstract partial class AutoController : AutoGetterController where TType : class, new() where TGetAllResponse : IAutomappedAttribute, new() where TCreateRequest : IAutomappedAttribute, new() @@ -48,24 +42,15 @@ namespace NejCommon.Controllers where TUpdateRequest : IAutomappedAttribute, new() where TUpdateResponse : IAutomappedAttribute, new() { - protected readonly AppDbContext db; - protected readonly IServiceProvider providers; - public AutoController(AppDbContext appDb, IServiceProvider providers) : base() + public AutoController(CommonDbContext appDb, IServiceProvider providers) : base(appDb, providers) { - db = appDb; - this.providers = providers; + } - protected abstract IQueryable GetQuery(TOwner comp); protected virtual TType AssociateWithParent(TType entity, TOwner comp) { - if (typeof(TOwner) != typeof(Company)) - { - throw new NotImplementedException("Special parent association not implemented"); - } - - var props = typeof(TType).GetProperty("Company"); + var props = typeof(TType).GetProperty(HelperMainName); if (props == null) { //not implemented @@ -74,26 +59,6 @@ namespace NejCommon.Controllers props.SetValue(entity, comp); return entity; } - protected virtual IQueryable ApplyDefaultOrdering(IQueryable query) - { - return query; - } - - - /// - /// Get all the - /// - /// Success - /// There was an error - [HttpGet] - public virtual async Task, Ok>>> GetAll(TOwner company, [FromQuery] Pagination pag) - { - var data = await ApplyDefaultOrdering(GetQuery(company)).AsNoTrackingWithIdentityResolution().ApplyPaginationRes(providers, pag); - - //Console.Writeline(data.Data); - - return TypedResults.Ok(data); - } /// /// Creates the @@ -102,7 +67,7 @@ namespace NejCommon.Controllers /// Success /// There was an error [HttpPost] - public virtual async Task, CreatedAtRoute>> Create(TOwner company, [FromBody] TCreateRequest body) + public virtual async Task, CreatedAtRoute>> Create([FromServices] TOwner company, [FromBody] TCreateRequest body) { var entity = db.Create(); @@ -115,20 +80,6 @@ namespace NejCommon.Controllers return await db.ApiSaveChangesAsyncCreate(providers, entity); } - /// - /// Gets the - /// - /// - /// Success - /// There was an error - [HttpGet] - [Route("{id}/")] - public virtual async Task>> Get([FromServices][ModelBinder(Name = "id")] TType entity) - { - var dat = new TGetResponse().ApplyFrom(providers, entity); - return TypedResults.Ok(dat); - } - /// /// Delete company. /// @@ -154,10 +105,7 @@ namespace NejCommon.Controllers [HttpPut] [Route("{id}/")] public virtual async Task, Ok>> Update([FromServices][ModelBinder(Name = "id")] TType entity, [FromBody] TUpdateRequest body) - {/* - if(entity is InvoiceBase inv){ - //Console.Writeline(inv.InvoiceItems.Count); - }*/ + { body.ApplyTo(providers, entity); var dat = new TUpdateResponse().ApplyFrom(providers, entity); @@ -169,4 +117,55 @@ namespace NejCommon.Controllers return (Results, Ok>)ctor.Invoke(new object[] { res.Result }); } } + [ApiController] + public abstract class AutoGetterController : ControllerBase + where TType : class, new() + where TGetAllResponse : IAutomappedAttribute, new() + where TGetResponse : IAutomappedAttribute, new() + { + protected readonly CommonDbContext db; + protected readonly IServiceProvider providers; + + public AutoGetterController(CommonDbContext appDb, IServiceProvider providers) : base() + { + db = appDb; + this.providers = providers; + } + + protected abstract IQueryable GetQuery(TOwner comp); + + protected virtual IQueryable ApplyDefaultOrdering(IQueryable query) + { + return query; + } + + /// + /// Get all the + /// + /// Success + /// There was an error + [HttpGet] + public virtual async Task, Ok>>> GetAll([FromServices] TOwner company, [FromQuery] Pagination pag) + { + var data = await ApplyDefaultOrdering(GetQuery(company)).AsNoTrackingWithIdentityResolution().ApplyPaginationRes(providers, pag); + + //Console.Writeline(data.Data); + + return TypedResults.Ok(data); + } + + /// + /// Gets the + /// + /// + /// Success + /// There was an error + [HttpGet] + [Route("{id}/")] + public virtual async Task>> Get([FromServices][ModelBinder(Name = "id")] TType entity) + { + var dat = new TGetResponse().ApplyFrom(providers, entity); + return TypedResults.Ok(dat); + } + } } diff --git a/Emails/wwwroot/output.css b/Emails/wwwroot/output.css index 59fba95..b596b6e 100644 --- a/Emails/wwwroot/output.css +++ b/Emails/wwwroot/output.css @@ -1,5 +1,5 @@ /* -! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com +! tailwindcss v3.4.11 | MIT License | https://tailwindcss.com */ /* @@ -31,9 +31,12 @@ 3. Use a more readable tab size. 4. Use the user's configured `sans` font-family by default. 5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS */ -html { +html, +:host { line-height: 1.5; /* 1 */ -webkit-text-size-adjust: 100%; @@ -43,10 +46,14 @@ html { -o-tab-size: 4; tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ } /* @@ -118,8 +125,10 @@ strong { } /* -1. Use the user's configured `mono` font family by default. -2. Correct the odd `em` font sizing in all browsers. +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. */ code, @@ -128,8 +137,12 @@ samp, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ - font-size: 1em; + font-feature-settings: normal; /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ } /* @@ -188,12 +201,18 @@ select, textarea { font-family: inherit; /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ font-size: 100%; /* 1 */ font-weight: inherit; /* 1 */ line-height: inherit; /* 1 */ + letter-spacing: inherit; + /* 1 */ color: inherit; /* 1 */ margin: 0; @@ -217,9 +236,9 @@ select { */ button, -[type='button'], -[type='reset'], -[type='submit'] { +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { -webkit-appearance: button; /* 1 */ background-color: transparent; @@ -338,6 +357,14 @@ menu { padding: 0; } +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + /* Prevent resizing textareas horizontally by default. */ @@ -433,6 +460,9 @@ video { --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; @@ -464,6 +494,10 @@ video { --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; } ::backdrop { @@ -480,6 +514,9 @@ video { --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; @@ -511,234 +548,28 @@ video { --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; } -.container { - width: 100%; -} - -@media (min-width: 640px) { - .container { - max-width: 640px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 1024px; - } -} - -@media (min-width: 1280px) { - .container { - max-width: 1280px; - } -} - -@media (min-width: 1536px) { - .container { - max-width: 1536px; - } +.visible { + visibility: visible; } .static { position: static; } -.ml-2 { - margin-left: 0.5rem; -} - .inline { display: inline; } -.flex { - display: flex; -} - .table { display: table; } -.h-full { - height: 100%; -} - -.h-16 { - height: 4rem; -} - -.h-48 { - height: 12rem; -} - -.w-full { - width: 100%; -} - -.w-16 { - width: 4rem; -} - -.w-48 { - width: 12rem; -} - -.flex-grow { - flex-grow: 1; -} - -.flex-col { - flex-direction: column; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.items-end { - align-items: flex-end; -} - -.items-center { - align-items: center; -} - -.justify-end { - justify-content: flex-end; -} - -.justify-center { - justify-content: center; -} - -.justify-between { - justify-content: space-between; -} - -.self-end { - align-self: flex-end; -} - -.rounded-md { - border-radius: 0.375rem; -} - -.rounded { - border-radius: 0.25rem; -} - -.border-4 { - border-width: 4px; -} - -.bg-primary { - --tw-bg-opacity: 1; - background-color: rgb(var(--primary) / var(--tw-bg-opacity)); -} - -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.px-10 { - padding-left: 2.5rem; - padding-right: 2.5rem; -} - -.pt-2 { - padding-top: 0.5rem; -} - -.pb-2 { - padding-bottom: 0.5rem; -} - -.pl-8 { - padding-left: 2rem; -} - -.pr-8 { - padding-right: 2rem; -} - -.pr-4 { - padding-right: 1rem; -} - -.pt-1 { - padding-top: 0.25rem; -} - -.text-left { - text-align: left; -} - -.text-right { - text-align: right; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} - -.text-3xl { - font-size: 1.875rem; - line-height: 2.25rem; -} - -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} - -.text-2xl { - font-size: 1.5rem; - line-height: 2rem; -} - -.font-semibold { - font-weight: 600; -} - -.font-bold { - font-weight: 700; -} - -.text-secondary { - --tw-text-opacity: 1; - color: rgb(var(--secondary-text) / var(--tw-text-opacity)); -} - -.text-primary { - --tw-text-opacity: 1; - color: rgb(var(--primary-text) / var(--tw-text-opacity)); -} - .filter { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } diff --git a/Models/Api/ModelBinder.cs b/Models/Api/ModelBinder.cs index 7bbace6..660ea65 100644 --- a/Models/Api/ModelBinder.cs +++ b/Models/Api/ModelBinder.cs @@ -11,8 +11,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; -using NejAccountingAPI.Documents; -using NejAccountingAPI.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Linq.Expressions; using System.Reflection; @@ -54,7 +52,7 @@ namespace NejCommon.Models public abstract class EntityBinder : EntityBinder where TEntity : class { - public EntityBinder(AppDbContext db, RouteValue[] routeValues) : base(db, routeValues) + public EntityBinder(DbContext db, RouteValue[] routeValues) : base(db, routeValues) { } } @@ -65,13 +63,13 @@ namespace NejCommon.Models where TEntity : class where TIdType : IEquatable { - private readonly AppDbContext _db; + private readonly DbContext _db; private readonly RouteValue[] _routeValues; public RouteValue[] RouteValues => _routeValues; - public EntityBinder(AppDbContext db, RouteValue[] routeValues) + public EntityBinder(DbContext db, RouteValue[] routeValues) { _db = db; _routeValues = routeValues; diff --git a/Models/CommonDbContext.cs b/Models/CommonDbContext.cs new file mode 100644 index 0000000..1b36e3c --- /dev/null +++ b/Models/CommonDbContext.cs @@ -0,0 +1,145 @@ + + +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapPropertyHelper; +using EntityFrameworkCore.Projectables; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.EntityFrameworkCore; + +namespace NejCommon.Models; + + + public abstract class CommonDbContext : DbContext + { + public CommonDbContext() : base() + { + } + public CommonDbContext(DbContextOptions options) + : base(options) + { + } + + public async Task ApiSaveChangesAsync() + { + try + { + await SaveChangesAsync(); + return true; + } + catch (Exception ex) + { + Console.WriteLine("Error saving db: " + ex.Message); + Console.WriteLine(ex.StackTrace); + return false; + } + } + + public static BadRequest SaveError = TypedResults.BadRequest(new Error + { + Message = "Error saving data to database" + }); + + public async Task, T1>> ApiSaveChangesAsync(T1 value) where T1 : IResult + { + var res = await ApiSaveChangesAsync(); + + if (res) + return value; + else + return SaveError; + } + public async Task, CreatedAtRoute>> ApiSaveChangesAsyncCreate(IServiceProvider providers, T1 value, bool apply = true) where T2 : IAutomappedAttribute, new() + { + if (!apply) + return TypedResults.CreatedAtRoute(new T2().ApplyFrom(providers, value)); + + var res = await ApiSaveChangesAsync(); + + if (res) + return TypedResults.CreatedAtRoute(new T2().ApplyFrom(providers, value)); + else + return SaveError; + } + public async Task, Ok>> ApiSaveChangesAsyncOk(IServiceProvider providers, T1 value, bool apply = true) where T2 : IAutomappedAttribute, new() + { + if (!apply) + return TypedResults.Ok(new T2().ApplyFrom(providers, value)); + + var res = await ApiSaveChangesAsync(); + + if (res) + return TypedResults.Ok(new T2().ApplyFrom(providers, value)); + else + return SaveError; + } + public async Task FindOrCreateAsync(Expression> predicate, Func factory) where T : class + { + var entity = ChangeTracker.Entries().Select(x => x.Entity).FirstOrDefault(predicate.Compile()); + + if (entity != null && Entry(entity).State == EntityState.Deleted) + { + Entry(entity).State = EntityState.Modified; + } + + if (entity == null) + { + entity = await Set().FirstOrDefaultAsync(predicate); + } + + if (entity == null) + { + var newAppDB = Activator.CreateInstance(this.GetType()) as CommonDbContext; + + entity = await newAppDB.Set().FirstOrDefaultAsync(predicate); + + //track the entity if it's not null and not already being tracked + if (entity != null && this.Entry(entity).State == EntityState.Detached) + Attach(entity); + } + if (entity == null) + { + var newEntity = factory(); + await this.AddAsync(newEntity); + entity = newEntity; + } + + return entity; + } + public T FindOrCreate(Expression> predicate, Func factory) where T : class + { + var entity = ChangeTracker.Entries().Where(e => e.State != EntityState.Deleted).Select(x => x.Entity).FirstOrDefault(predicate.Compile()); + //var entity = this.; + if (entity == null) + { + var newAppDB = Activator.CreateInstance(this.GetType()) as CommonDbContext; + + entity = newAppDB.Set().FirstOrDefault(predicate); + if (entity != null) + Attach(entity); + } + if (entity == null) + { + var newEntity = factory(); + this.Add(newEntity); + entity = newEntity; + } + + return entity; + } + + public T Create(Action config = null, params object[] constructorArguments) + { + var entity = this.CreateProxy(constructorArguments); + + config?.Invoke(entity); + this.Add(entity); + return entity; + } + + public void ApplyRelationships() + { + this.ChangeTracker.DetectChanges(); + } + } \ No newline at end of file diff --git a/Utils/Extensions.cs b/Utils/Extensions.cs index 45235a2..bce8985 100644 --- a/Utils/Extensions.cs +++ b/Utils/Extensions.cs @@ -4,7 +4,6 @@ using AutoMapPropertyHelper; using EntityFrameworkCore.Projectables; using EntityFrameworkCore.Projectables.Extensions; using Microsoft.EntityFrameworkCore; -using NejAccountingAPI.Models; using NejCommon.Controllers; using NejCommon.Models; @@ -56,7 +55,7 @@ public static class Extensions } var extPar = Expression.Parameter(typeof(TType)); - var binder = (TEntityBinder)typeof(TEntityBinder).GetConstructor(new[] { typeof(AppDbContext) })!.Invoke(new object?[] { null }); + var binder = (TEntityBinder)typeof(TEntityBinder).GetConstructor(new[] { typeof(DbContext) })!.Invoke(new object?[] { null }); var idMatcher = binder.RouteValues.First(x => x.Primary).Expression; var reducedIdMatcher = Expression.Lambda>(Expression.Invoke(idMatcher, extPar, Expression.Constant(search)).Reduce(), extPar); //Console.Writeline(reducedIdMatcher);