diff --git a/TenantFile/Api/Models/Entities/Address.cs b/TenantFile/Api/Models/Entities/Address.cs index 2098d60..7a96545 100644 --- a/TenantFile/Api/Models/Entities/Address.cs +++ b/TenantFile/Api/Models/Entities/Address.cs @@ -11,21 +11,13 @@ public class Address : IEntity { public int Id { get; set; } - - [XmlElement("Address2")] - public string Line1 { get; set; } = null!; - [XmlElement("Address1")] - public string? Line2 { get; set; } + public string Line1 { get; set; } = null!; + public string? Line2 { get; set; } public string? Line3 { get; set; } public string? Line4 { get; set; } - [XmlElement("City")] public string City { get; set; } = null!; - [XmlElement("State")] public string State { get; set; } = null!; - [XmlElement("Zip5")] public string PostalCode { get; set; } = null!; - - [XmlElement("ReturnText")] public string? ValidationMessage { get; set; } } } \ No newline at end of file diff --git a/TenantFile/Api/Models/Entities/Organizer.cs b/TenantFile/Api/Models/Entities/Organizer.cs index b02f205..43ba966 100644 --- a/TenantFile/Api/Models/Entities/Organizer.cs +++ b/TenantFile/Api/Models/Entities/Organizer.cs @@ -5,12 +5,12 @@ namespace TenantFile.Api.Models.Entities { public class Organizer { - + public string Uid { get; set; } = null!; public string Name { get; set; } = null!; - public virtual ICollection Phones{ get; set; } = null!; - public virtual ICollection Properties { get; set; } = null!; + public ICollection Phones { get; set; } = new List(); + public ICollection Properties { get; set; } = new List(); } } \ No newline at end of file diff --git a/TenantFile/Api/Models/Entities/Phone.cs b/TenantFile/Api/Models/Entities/Phone.cs index e573f17..5e19268 100644 --- a/TenantFile/Api/Models/Entities/Phone.cs +++ b/TenantFile/Api/Models/Entities/Phone.cs @@ -8,12 +8,12 @@ namespace TenantFile.Api.Models.Entities { public class Phone : IEntity { - + public int Id { get; set; } public string PhoneNumber { get; set; } = null!; - public virtual ICollection Tenants { get; set; } = null!; - public virtual ICollection Images { get; set; } = null!; + public ICollection Tenants { get; set; } = new List(); + public ICollection Images { get; set; } = new List(); } } \ No newline at end of file diff --git a/TenantFile/Api/Models/Entities/Property.cs b/TenantFile/Api/Models/Entities/Property.cs index 9f4f5ce..f35b8c5 100644 --- a/TenantFile/Api/Models/Entities/Property.cs +++ b/TenantFile/Api/Models/Entities/Property.cs @@ -5,16 +5,16 @@ using TenantFile.Api.Common; using TenantFile.Api.Models.Entities; -namespace TenantFile.Api.Models.Entities +namespace TenantFile.Api.Models.Entities { public class Property : IEntity { public int Id { get; set; } public string? Name { get; set; } - public int AddressId { get; set; } - public virtual Address Address { get; set; } = null!; + public int AddressId { get; set; } + public Address Address { get; set; } = null!; - public virtual ICollection Residences { get; set; } = null!; + public ICollection Residences { get; set; } = new List(); //public virtual Complex Complex { get; set; } = null!; } } \ No newline at end of file diff --git a/TenantFile/Api/Models/Entities/Residence.cs b/TenantFile/Api/Models/Entities/Residence.cs index 42cf641..3a0b7f6 100644 --- a/TenantFile/Api/Models/Entities/Residence.cs +++ b/TenantFile/Api/Models/Entities/Residence.cs @@ -11,9 +11,9 @@ public class Residence : IEntity public int Id { get; set; } public int? PropertyId { get; set; } - public virtual Property? Property { get; set; } + public Property? Property { get; set; } public int AddressId { get; set; } - public virtual Address Address { get; set; } = null!; + public Address Address { get; set; } = null!; } } \ No newline at end of file diff --git a/TenantFile/Api/Models/Entities/ResidenceRecord.cs b/TenantFile/Api/Models/Entities/ResidenceRecord.cs index de04724..10771f9 100644 --- a/TenantFile/Api/Models/Entities/ResidenceRecord.cs +++ b/TenantFile/Api/Models/Entities/ResidenceRecord.cs @@ -15,8 +15,8 @@ public class ResidenceRecord : IEntity public DateTime MoveIn { get; set; } public DateTime MoveOut { get; set; } - public virtual Tenant Tenant { get; set; } = null!; - public virtual Residence Residence { get; set; } = null!; + public Tenant Tenant { get; set; } = null!; + public Residence Residence { get; set; } = null!; } diff --git a/TenantFile/Api/Models/Entities/Tenant.cs b/TenantFile/Api/Models/Entities/Tenant.cs index fea0191..df92936 100644 --- a/TenantFile/Api/Models/Entities/Tenant.cs +++ b/TenantFile/Api/Models/Entities/Tenant.cs @@ -14,6 +14,6 @@ public class Tenant : IEntity public int? ResidenceId { get; set; } public Residence? CurrentResidence { get; set; } - public virtual ICollection Phones { get; set; } = null!; + public ICollection Phones { get; set; } = new List(); } } \ No newline at end of file diff --git a/TenantFile/Api/Models/GraphQL/Addresses/AddressMutations.cs b/TenantFile/Api/Models/GraphQL/Addresses/AddressMutations.cs new file mode 100644 index 0000000..5c787a4 --- /dev/null +++ b/TenantFile/Api/Models/GraphQL/Addresses/AddressMutations.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using HotChocolate.Types; +using TenantFile.Api.Models.Tenants; +using TenantFile.Api.Extensions; +using TenantFile.Api.Models.Entities; +using TenantFile.Api.Services; +using TenantFile.Api.Models.Addresses; + +namespace TenantFile.Api.Models.Properties +{ + [ExtendObjectType(Name = "Mutation")] + public class AddressMutations + { + public async Task VerifyAddress([Service] IAddressVerificationService service, VerifyAddressInput address) + { + return new VerifyAddressPayload(await service.VerifyAddressAsync(address)); + } + } +} \ No newline at end of file diff --git a/TenantFile/Api/Models/GraphQL/Addresses/AddressQueries.cs b/TenantFile/Api/Models/GraphQL/Addresses/AddressQueries.cs index 6494828..7f70353 100644 --- a/TenantFile/Api/Models/GraphQL/Addresses/AddressQueries.cs +++ b/TenantFile/Api/Models/GraphQL/Addresses/AddressQueries.cs @@ -11,6 +11,7 @@ using TenantFile.Api.DataLoader; using TenantFile.Api.Extensions; using TenantFile.Api.Models.Entities; +using HotChocolate.Types.Relay; namespace TenantFile.Api.Models.Addresses { @@ -23,7 +24,7 @@ public class AddressQueries [HotChocolate.Data.UseSorting] public IQueryable
GetAddresses([ScopedService] TenantFileContext tenantContext) => tenantContext.Addresses.AsQueryable(); - public Task
GetAddressAsync(int id, AddressByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); + public Task
GetAddressAsync([ID(nameof(Address))] int id, AddressByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); } } diff --git a/TenantFile/Api/Models/GraphQL/Addresses/AddressType.cs b/TenantFile/Api/Models/GraphQL/Addresses/AddressType.cs index b2b9717..6852244 100644 --- a/TenantFile/Api/Models/GraphQL/Addresses/AddressType.cs +++ b/TenantFile/Api/Models/GraphQL/Addresses/AddressType.cs @@ -26,7 +26,6 @@ protected override void Configure(IObjectTypeDescriptor
descriptor) public class AddressResolvers { public async Task
GetAddress(Address address, - //[ScopedService] TenantFileContext context, AddressByIdDataLoader dataLoader, CancellationToken cancellationToken) { diff --git a/TenantFile/Api/Models/GraphQL/Addresses/VerifyAddressInput.cs b/TenantFile/Api/Models/GraphQL/Addresses/VerifyAddressInput.cs new file mode 100644 index 0000000..e5388f4 --- /dev/null +++ b/TenantFile/Api/Models/GraphQL/Addresses/VerifyAddressInput.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TenantFile.Api.Models.Addresses +{ + public record VerifyAddressInput + ( + string? Line1, + string? Line2, + string? Line3, + string? Line4, + string? City, + string? State, + string? PostalCode + ); +} diff --git a/TenantFile/Api/Models/GraphQL/Addresses/VerifyAddressPayload.cs b/TenantFile/Api/Models/GraphQL/Addresses/VerifyAddressPayload.cs new file mode 100644 index 0000000..0d48e2a --- /dev/null +++ b/TenantFile/Api/Models/GraphQL/Addresses/VerifyAddressPayload.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; +using TenantFile.Api.Common; +using TenantFile.Api.Models.Entities; +using TenantFile.Api.Services; + +namespace TenantFile.Api.Models.Addresses +{ + public class VerifyAddressPayload : TPayload + { + + public VerifyAddressPayload(USPSVerifyPayload address) : base(address) + { + + } + public VerifyAddressPayload(UserError error) + : base(new[] { error }) + { + } + } +} \ No newline at end of file diff --git a/TenantFile/Api/Models/GraphQL/Images/AddImageInput.cs b/TenantFile/Api/Models/GraphQL/Images/AddImageInput.cs index 74da1e6..fc26c9d 100644 --- a/TenantFile/Api/Models/GraphQL/Images/AddImageInput.cs +++ b/TenantFile/Api/Models/GraphQL/Images/AddImageInput.cs @@ -1,13 +1,17 @@ -using TenantFile.Api.Models.Entities; +using HotChocolate.Types.Relay; +using TenantFile.Api.Models.Entities; namespace TenantFile.Api.Models.Images { public record AddImageInput( string FileName, - Tenant Tenant, - Residence Residence, + string ThumbnailName, + [ID(nameof(Tenant))] + int Tenant, + [ID(nameof(Residence))] + int Residence, ImageLabel[] Labels ); - + } diff --git a/TenantFile/Api/Models/GraphQL/Images/ImageMutations.cs b/TenantFile/Api/Models/GraphQL/Images/ImageMutations.cs index b4a717f..00023bf 100644 --- a/TenantFile/Api/Models/GraphQL/Images/ImageMutations.cs +++ b/TenantFile/Api/Models/GraphQL/Images/ImageMutations.cs @@ -13,19 +13,37 @@ namespace TenantFile.Api.Models.Images [ExtendObjectType(Name = "Mutation")] public class ImageMutations {/// - /// Intended for use by Organizer uploading Images via dashboard, not for SMS/Twilio uploads - /// - /// - /// - /// + /// Intended for use by Organizer uploading Images via dashboard, not for SMS/Twilio uploads + /// The relationshiup of the image and the residence is implied as of 2/11 would need to add a Property on the image or Residence Entity + /// + /// + /// + /// [UseTenantFileContext] public async Task AddImageAsync( AddImageInput input, [ScopedService] TenantFileContext context) { - var image = new Image{ Name = input.FileName }; + var (fileName, thumb, tenantId, residenceId, labels) = input; + + var image = new Image + { + Name = fileName, + Labels = labels, + ThumbnailName = thumb, + + }; context.Images.Add(image); + + var phones + = context.Phones.AsQueryable() + .Where(p => p.Tenants.Select(t => t.Id).Contains(tenantId)).ToList(); + phones.ForEach(p => p.Images.Add(image)); + + + + await context.SaveChangesAsync(); return new AddImagePayload(image); } diff --git a/TenantFile/Api/Models/GraphQL/Images/ImageQueries.cs b/TenantFile/Api/Models/GraphQL/Images/ImageQueries.cs index 8908436..de89957 100644 --- a/TenantFile/Api/Models/GraphQL/Images/ImageQueries.cs +++ b/TenantFile/Api/Models/GraphQL/Images/ImageQueries.cs @@ -7,6 +7,7 @@ using HotChocolate; using HotChocolate.Data; using HotChocolate.Types; +using HotChocolate.Types.Relay; using Microsoft.EntityFrameworkCore; using TenantFile.Api.DataLoader; using TenantFile.Api.Extensions; @@ -23,6 +24,6 @@ public class ImageQueries [HotChocolate.Data.UseSorting] public IQueryable GetImages([ScopedService] TenantFileContext tenantContext) => tenantContext.Images.AsNoTracking(); - public Task GetImageAsync(int id, ImageByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); + public Task GetImageAsync([ID(nameof(Image))] int id, ImageByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); } } diff --git a/TenantFile/Api/Models/GraphQL/Phones/PhoneQueries.cs b/TenantFile/Api/Models/GraphQL/Phones/PhoneQueries.cs index d601056..27298a5 100644 --- a/TenantFile/Api/Models/GraphQL/Phones/PhoneQueries.cs +++ b/TenantFile/Api/Models/GraphQL/Phones/PhoneQueries.cs @@ -12,29 +12,37 @@ using TenantFile.Api.DataLoader; using TenantFile.Api.Extensions; using TenantFile.Api.Models.Entities; +using HotChocolate.Types.Relay; namespace TenantFile.Api.Models.Phones { - [ExtendObjectType(Name = "Query")] - public class PhoneQueries - { + [ExtendObjectType(Name = "Query")] + public class PhoneQueries + { - [UseTenantFileContext] - [UsePaging] - [UseProjection] - [HotChocolate.Data.UseFiltering(typeof(PhoneFilterInputType))] - [HotChocolate.Data.UseSorting] - public IQueryable GetPhones([ScopedService] TenantFileContext tenantContext) => tenantContext.Phones.AsQueryable(); + [UseTenantFileContext] + [UsePaging(typeof(NonNullType))] + // [UseProjection] + [HotChocolate.Data.UseFiltering(typeof(PhoneFilterInputType))] + [HotChocolate.Data.UseSorting] + public IQueryable GetPhones([ScopedService] TenantFileContext tenantContext) => tenantContext.Phones.AsQueryable().Include(p => p.Tenants); - [UseTenantFileContext] - public async Task> GetTenantlessPhonesAsync( - [ScopedService] TenantFileContext tenantContext) => - await tenantContext.Phones - .AsQueryable() - .Where(p => p.Tenants.Count == 0)//default for int is 0 so if Tenants==null, it should still return 0. Evaluating null Tenant list throws implementation exception - .ToListAsync(); + public async Task GetPhoneAsync([ID(nameof(Phone))] int id, + PhoneByIdDataLoader dataLoader, + CancellationToken cancellationToken) => await dataLoader.LoadAsync(id, cancellationToken); + public async Task> GetPhonesAsync([ID(nameof(Phone))] int[] ids, + PhoneByIdDataLoader dataLoader, + CancellationToken cancellationToken) => await dataLoader.LoadAsync(ids, cancellationToken); + [UseTenantFileContext] + public async Task> GetTenantlessPhonesAsync( + [ScopedService] TenantFileContext tenantContext) => + await tenantContext.Phones + .AsQueryable() + .Where(p => p.Tenants.Count == 0)//default for int is 0 so if Tenants==null, it should still return 0. Evaluating null Tenant list throws implementation exception + .ToListAsync(); - } + + } } diff --git a/TenantFile/Api/Models/GraphQL/Phones/PhoneType.cs b/TenantFile/Api/Models/GraphQL/Phones/PhoneType.cs index 2e090fb..281c52c 100644 --- a/TenantFile/Api/Models/GraphQL/Phones/PhoneType.cs +++ b/TenantFile/Api/Models/GraphQL/Phones/PhoneType.cs @@ -11,6 +11,7 @@ using TenantFile.Api.DataLoader; using TenantFile.Api.Extensions; using TenantFile.Api.Models.Entities; +using TenantFile.Api.Models.Tenants; namespace TenantFile.Api.Models.Phones { @@ -19,22 +20,29 @@ public class PhoneType : ObjectType protected override void Configure(IObjectTypeDescriptor descriptor) { - //descriptor - // .ImplementsNode() - // .IdField(t => t.Id) - // .ResolveNode((ctx, id) => ctx.DataLoader>().LoadAsync(id, ctx.RequestAborted)); + descriptor + .ImplementsNode() + .IdField(t => t.Id) + .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); descriptor - .Field(p => p.Images) - .ResolveWith(r => r.GetImagesAsync(default!, default!, default!, default!)) + .Field(p => p.Images) + .ResolveWith(r => r.GetImagesAsync(default!, default!, default!, default)) .UseTenantContext() .Name("images"); - + descriptor + .Field(p => p.Tenants) + .ResolveWith(r => r.GetTenantsAsync(default!, default!, default!, default)) + .UseTenantContext() + // .UsePaging>()//You can only use this on one of the + .Name("tenants"); + } } class PhoneResolvers { + [UseTenantFileContext] public async Task> GetImagesAsync( Phone phone, [ScopedService] TenantFileContext context, @@ -49,5 +57,21 @@ public async Task> GetImagesAsync( return await dataLoader.LoadAsync(cancellationToken, await imagesId); } + [UseTenantFileContext] + public async Task> GetTenantsAsync( + Phone phone, + [ScopedService] TenantFileContext context, + TenantByIdDataLoader dataLoader, + CancellationToken cancellationToken) + { + int[] phoneIds = await context.Phones.AsQueryable() + .Where(p => p.Id == phone.Id) + //.Include(e => e.Tenants) + .SelectMany(p => p.Tenants.Select(i => i.Id)) + .ToArrayAsync(); + + return await dataLoader.LoadAsync(phoneIds, cancellationToken); + + } } } diff --git a/TenantFile/Api/Models/GraphQL/Properties/PropertyMutations.cs b/TenantFile/Api/Models/GraphQL/Properties/PropertyMutations.cs index b70cfc6..32f982e 100644 --- a/TenantFile/Api/Models/GraphQL/Properties/PropertyMutations.cs +++ b/TenantFile/Api/Models/GraphQL/Properties/PropertyMutations.cs @@ -6,9 +6,11 @@ using HotChocolate; using HotChocolate.Subscriptions; using HotChocolate.Types; +using TenantFile.Api.Models.Tenants; using TenantFile.Api.Extensions; using TenantFile.Api.Models.Entities; using TenantFile.Api.Services; +using TenantFile.Api.Models.Addresses; namespace TenantFile.Api.Models.Properties { @@ -18,7 +20,7 @@ public class PropertyMutations [UseTenantFileContext] public async Task CreateProperty(CreatePropertyInput input, [ScopedService] TenantFileContext context) - + { var property = new Property { @@ -41,9 +43,5 @@ public async Task CreateProperty(CreatePropertyInput inpu return new CreatePropertyPayload(property); } - public Task VerifyAddress([Service]IAddressVerificationService service, Address address) - { - return service.VerifyAddressAsync(address); - } } } diff --git a/TenantFile/Api/Models/GraphQL/Properties/PropertyQueries.cs b/TenantFile/Api/Models/GraphQL/Properties/PropertyQueries.cs index 2e387f1..6699ab8 100644 --- a/TenantFile/Api/Models/GraphQL/Properties/PropertyQueries.cs +++ b/TenantFile/Api/Models/GraphQL/Properties/PropertyQueries.cs @@ -11,6 +11,7 @@ using TenantFile.Api.Extensions; using TenantFile.Api.Models.Entities; using TenantFile.Api.Services; +using HotChocolate.Types.Relay; namespace TenantFile.Api.Models.Properties { @@ -23,7 +24,7 @@ public class PropertyQueries [HotChocolate.Data.UseSorting] public IQueryable GetProperties([ScopedService] TenantFileContext tenantContext) => tenantContext.Properties.AsQueryable(); - public Task GetPropertyAsync(int id, PropertyByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); + public Task GetPropertyAsync([ID(nameof(Property))] int id, PropertyByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); } diff --git a/TenantFile/Api/Models/GraphQL/Residences/ResidenceQueries.cs b/TenantFile/Api/Models/GraphQL/Residences/ResidenceQueries.cs index 2ea4d7c..7f06c47 100644 --- a/TenantFile/Api/Models/GraphQL/Residences/ResidenceQueries.cs +++ b/TenantFile/Api/Models/GraphQL/Residences/ResidenceQueries.cs @@ -7,6 +7,7 @@ using HotChocolate; using HotChocolate.Data; using HotChocolate.Types; +using HotChocolate.Types.Relay; using TenantFile.Api.DataLoader; using TenantFile.Api.Extensions; using TenantFile.Api.Models.Entities; @@ -22,7 +23,7 @@ public class ResidenceQueries [HotChocolate.Data.UseSorting] public IQueryable GetResidences([ScopedService] TenantFileContext tenantContext) => tenantContext.Residences.AsQueryable(); - public Task GetResidenceByIdAsync(int id, ResidenceByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); - public async Task> GetResidencesByIdAsync(int[] ids, ResidenceByIdDataLoader dataLoader, CancellationToken cancellationToken) => await dataLoader.LoadAsync(ids, cancellationToken); + public Task GetResidenceByIdAsync([ID(nameof(Residence))] int id, ResidenceByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); + public async Task> GetResidencesByIdAsync([ID(nameof(Residence))] int[] ids, ResidenceByIdDataLoader dataLoader, CancellationToken cancellationToken) => await dataLoader.LoadAsync(ids, cancellationToken); } } \ No newline at end of file diff --git a/TenantFile/Api/Models/GraphQL/Residences/ResidenceType.cs b/TenantFile/Api/Models/GraphQL/Residences/ResidenceType.cs index 2e1b617..8c5de93 100644 --- a/TenantFile/Api/Models/GraphQL/Residences/ResidenceType.cs +++ b/TenantFile/Api/Models/GraphQL/Residences/ResidenceType.cs @@ -23,6 +23,13 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .ImplementsNode() .IdField(t => t.Id) .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); + descriptor + .Field(r => r.AddressId) + .ID(nameof(Address)); + descriptor + .Field(r => r.PropertyId) + .ID(nameof(Property)); + descriptor.Field(r => r.Address) .ResolveWith(r => r.GetAddressAsync(default!, default!, default!)) @@ -37,6 +44,7 @@ protected override void Configure(IObjectTypeDescriptor descriptor) public class ResidenceResolvers { + [UseTenantFileContext] public IQueryable GetResidence( Residence residence, [ScopedService] TenantFileContext context) @@ -55,7 +63,7 @@ public async Task
GetAddressAsync( Residence residence, PropertyByIdDataLoader dataLoader, CancellationToken cancellationToken) - { + { if (residence.PropertyId == null) { return null; diff --git a/TenantFile/Api/Models/GraphQL/Tenants/CreateTenantInput.cs b/TenantFile/Api/Models/GraphQL/Tenants/CreateTenantInput.cs index 9c12994..44ce9c9 100644 --- a/TenantFile/Api/Models/GraphQL/Tenants/CreateTenantInput.cs +++ b/TenantFile/Api/Models/GraphQL/Tenants/CreateTenantInput.cs @@ -1,10 +1,13 @@ -using TenantFile.Api.Models.Residences; +using HotChocolate.Types.Relay; +using TenantFile.Api.Models.Entities; +using TenantFile.Api.Models.Residences; namespace TenantFile.Api.Models.Tenants { - public record CreateTenantInput( - string Name, - string PhoneNumber, - CreateResidenceInput? CurrentResidence - ); + public record CreateTenantInput( + string Name, + string PhoneNumber, + CreateResidenceInput? CurrentResidence, + [ID(nameof(Residence))] int? ResidenceId //this can be handled by the Residence Input if reconfigured + ){} } diff --git a/TenantFile/Api/Models/GraphQL/Tenants/TenantMutations.cs b/TenantFile/Api/Models/GraphQL/Tenants/TenantMutations.cs index 0b671d0..2d7af5b 100644 --- a/TenantFile/Api/Models/GraphQL/Tenants/TenantMutations.cs +++ b/TenantFile/Api/Models/GraphQL/Tenants/TenantMutations.cs @@ -20,24 +20,48 @@ public async Task CreateTenantAsync( [ScopedService] TenantFileContext context, CancellationToken cancellationToken) { + var (nameInput, phoneInput, residenceInput, residenceIdInput) = inputTenant; // See if the phone number exists already Phone? phone = context.Phones.FirstOrDefault(x => x.PhoneNumber == inputTenant.PhoneNumber); // If it doesn't exist, create a new phone number - if (phone == null) - { - phone = new Phone - { - PhoneNumber = inputTenant.PhoneNumber - }; - } - var tenant = new Tenant - { - Name = inputTenant.Name, - Phones = new List { + if (phone == null) + { + phone = new Phone + { + PhoneNumber = phoneInput + }; + } + var tenant = new Tenant + { + Name = nameInput, + Phones = new List { phone }, - }; + }; + + if (residenceInput != null) + { + var (line1, line2, line3, line4, city, state, postalCode) = residenceInput!.AddressInput; + tenant.CurrentResidence = new Residence() + { + Address = new Address() + { + Line1 = line1, + Line2 = line2, + Line3 = line3, + Line4 = line4, + City = city, + State = state, + PostalCode = postalCode + + } + }; + } + else + { + tenant.ResidenceId = residenceIdInput!; + } var tenantEntry = context.Tenants.Add(tenant); await context.SaveChangesAsync(cancellationToken); diff --git a/TenantFile/Api/Models/GraphQL/Tenants/TenantQueries.cs b/TenantFile/Api/Models/GraphQL/Tenants/TenantQueries.cs index c90963b..077b3f9 100644 --- a/TenantFile/Api/Models/GraphQL/Tenants/TenantQueries.cs +++ b/TenantFile/Api/Models/GraphQL/Tenants/TenantQueries.cs @@ -9,6 +9,8 @@ using TenantFile.Api.Models; using TenantFile.Api.Models.Entities; using HotChocolate.Types.Relay; +using TenantFile.Api.Models.Tenants; +using System.Collections.Generic; namespace TenantFile.Api.Tenants { @@ -16,12 +18,13 @@ namespace TenantFile.Api.Tenants public class TenantQueries { [UseTenantFileContext] - [UsePaging] - [UseProjection] + [UsePaging(typeof(NonNullType))] + // [UseProjection] [HotChocolate.Data.UseFiltering(typeof(TenantFilterInputType))] [HotChocolate.Data.UseSorting] public IQueryable GetTenants([ScopedService] TenantFileContext tenantContext) => tenantContext.Tenants.AsQueryable(); - public Task GetTenantAsync([ID(nameof(Tenant))] int id, TenantByIdDataLoader dataLoader, CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken); + public async Task GetTenantAsync([ID(nameof(Tenant))] int id, TenantByIdDataLoader dataLoader, CancellationToken cancellationToken) => await dataLoader.LoadAsync(id, cancellationToken); + public async Task> GetTenantsAsync([ID(nameof(Tenant))] int[] ids, TenantByIdDataLoader dataLoader, CancellationToken cancellationToken) => await dataLoader.LoadAsync(ids, cancellationToken); } } diff --git a/TenantFile/Api/Models/GraphQL/Tenants/TenantType.cs b/TenantFile/Api/Models/GraphQL/Tenants/TenantType.cs index e51b35b..528e7cd 100644 --- a/TenantFile/Api/Models/GraphQL/Tenants/TenantType.cs +++ b/TenantFile/Api/Models/GraphQL/Tenants/TenantType.cs @@ -1,14 +1,18 @@ using HotChocolate; using HotChocolate.Resolvers; using HotChocolate.Types; +using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using TenantFile.Api.DataLoader; +using TenantFile.Api.Extensions; using TenantFile.Api.Models.Entities; +using TenantFile.Api.Models.Phones; namespace TenantFile.Api.Models.Tenants { @@ -23,22 +27,53 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); descriptor .Field(t => t.CurrentResidence) - .ResolveWith(resolver => resolver.GetResidenceAsync(default!, default!, default!)) + .ResolveWith(resolver => resolver.GetResidenceAsync(default!, default!, default)) + .UseTenantContext() .Name("residence"); + descriptor + .Field(t => t.Phones) + .ResolveWith(resolver => resolver.GetPhonesAsync(default!, default!, default!, default)) + .UseTenantContext() + .UsePaging>() + .Name("phones"); + descriptor + .Field(t => t.ResidenceId) + .ID(nameof(Residence)); } } public class TenantResolvers { - public Task? GetResidenceAsync( + public async Task GetResidenceAsync( Tenant tenant, ResidenceByIdDataLoader dataLoader, CancellationToken cancellationToken) { - if (tenant.ResidenceId != null) + if (tenant.ResidenceId is null) { - return dataLoader.LoadAsync((int)tenant.ResidenceId, cancellationToken); + return null; } - return null; + return await dataLoader.LoadAsync((int)tenant.ResidenceId, cancellationToken); + } + [UseTenantFileContext] + public async Task> GetPhonesAsync( + Tenant tenant, + [ScopedService] TenantFileContext dbContext, + PhoneByIdDataLoader dataLoader, + CancellationToken cancellationToken + ) + { + + int[] phoneIds = await dbContext.Tenants + .AsQueryable() + .Include(t => t.Phones) + .Where(t => t.Id == tenant.Id) + .SelectMany(t => t.Phones.Select(p => p.Id)) + .ToArrayAsync(); + + // var phoneIds = tenant.Phones.Select(p => p.Id ).ToArray(); + // phoneIds.ToList().ForEach(p => Debug.WriteLine($"Phone Id: {p}")); + // phoneIds.ToList().ForEach(p => Console.WriteLine($"Phone Id: {p}")); + return await dataLoader.LoadAsync(phoneIds, cancellationToken ); } } } \ No newline at end of file diff --git a/TenantFile/Api/Services/AddressVerificationService.cs b/TenantFile/Api/Services/AddressVerificationService.cs index f36f4e1..08e4d81 100644 --- a/TenantFile/Api/Services/AddressVerificationService.cs +++ b/TenantFile/Api/Services/AddressVerificationService.cs @@ -8,6 +8,7 @@ using System.Xml.Serialization; using Microsoft.Extensions.Configuration; using TenantFile.Api.Models.Entities; +using TenantFile.Api.Models.Addresses; namespace TenantFile.Api.Services { @@ -15,13 +16,42 @@ namespace TenantFile.Api.Services public class AddressValidateResponse { [XmlElement("Address")] - public Address Address { get; set; } = null!; - + public USPSVerifyPayload Address { get; set; } = null!; + } + + public class USPSVerifyPayload + { + [XmlElement(ElementName = "Address2")] + public string? Line1 { get; set; } + [XmlElement(ElementName = "Address1")] + public string? Line2 { get; set; } + // [XmlElement(ElementName = "Address3")] + // public string Line3 { get; set; } + // [XmlElement(ElementName = "Address4")] + // public string Line4 { get; set; } + [XmlElement(ElementName = "City")] + public string? City { get; set; } + [XmlElement(ElementName = "State")] + public string? State { get; set; } + [XmlElement(ElementName = "Zip5")] + public string? PostalCode { get; set; } + [XmlElement("ReturnText")] + public string? ReturnText { get; set; } + [XmlElement("Error")] + public USPSError? USPSError { get; set; } + } + + public class USPSError + { + [XmlElement("Number")] + public string? Number { get; set; } + [XmlElement("Description")] + public string? Description { get; set; } } public interface IAddressVerificationService { - Task VerifyAddressAsync(Address address); + Task VerifyAddressAsync(VerifyAddressInput address); } public class AddressVerificationService : IAddressVerificationService @@ -33,8 +63,9 @@ public AddressVerificationService(string userName) const string URIBASE = @"https://secure.shippingapis.com/ShippingAPI.dll"; private readonly string userName; - public async Task VerifyAddressAsync(Address address) + public async Task VerifyAddressAsync(VerifyAddressInput address) { + var serializer = new XmlSerializer(typeof(AddressValidateResponse)); var client = new HttpClient(); //TODO: Can you serialize this instead of interpolating it? @@ -44,29 +75,28 @@ public AddressVerificationService(string userName) { "XML", $@"
{address.Line2}{address.Line1}{address.City}{address.State}{address.PostalCode}
"} }; - var serializer = new XmlSerializer(typeof(AddressValidateResponse)); - var content = new FormUrlEncodedContent(body!); + var content = new FormUrlEncodedContent(body.AsEnumerable()!); var response = await client.PostAsync(URIBASE, content); AddressValidateResponse addressSerial = new AddressValidateResponse(); try { - var stream = await response.EnsureSuccessStatusCode().Content.ReadAsStreamAsync(); - addressSerial = (AddressValidateResponse)serializer.Deserialize(stream)!; - //ValdationMessage is null when address is fully successful but if more info is needed, it has a value - //addressSerial.Address.ValidationMessage = $"Status Code: {response.StatusCode} | Reason Phrase: {response.ReasonPhrase} "; - return addressSerial.Address; + var stream = await response.EnsureSuccessStatusCode().Content.ReadAsStreamAsync(); + addressSerial = (AddressValidateResponse)serializer.Deserialize(stream)!; + //ValdationMessage is null when address is fully successful but if more info is needed, it has a value + //addressSerial.Address.ValidationMessage = $"Status Code: {response.StatusCode} | Reason Phrase: {response.ReasonPhrase} "; + return addressSerial.Address!; } catch (HttpRequestException) { - return new Address() { ValidationMessage = $"{addressSerial.Address.ValidationMessage ?? "Could not validate address at this time"}" }; + return new USPSVerifyPayload() + { + ReturnText = $"{addressSerial.Address.ReturnText ?? "Could not validate address at this time"}" + }; } - - - } } } diff --git a/TenantFile/Api/Startup.cs b/TenantFile/Api/Startup.cs index d33c29a..4b9f776 100644 --- a/TenantFile/Api/Startup.cs +++ b/TenantFile/Api/Startup.cs @@ -70,6 +70,7 @@ public void ConfigureServices(IServiceCollection services) .AddType() .AddType() .AddType() + .AddType() .AddQueryType(d => d.Name("Query")) .AddType() .AddType() diff --git a/TenantFile/Api/TenantFile.Api.csproj b/TenantFile/Api/TenantFile.Api.csproj index b9634a4..2920827 100644 --- a/TenantFile/Api/TenantFile.Api.csproj +++ b/TenantFile/Api/TenantFile.Api.csproj @@ -29,47 +29,47 @@ - + - - - + + + - + - - + + - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - - + +