From 6e558a144ec7d3bf48181b57083f009d0d768dbf Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Wed, 8 Nov 2023 20:32:50 +0000 Subject: [PATCH] feat: implement SendAsync (find) with different query/response generic types --- src/FMData.Rest/FileMakerRestClient.cs | 35 ++++---------- src/FMData.Xml/FileMakerXmlClient.cs | 43 +++++------------ src/FMData/FileMakerApiClientBase.cs | 45 ++++++----------- src/FMData/IFileMakerApiClient.cs | 48 +++++++++---------- .../FMData.Rest.Tests/Find.SendAsync.Tests.cs | 29 +++++++++++ 5 files changed, 88 insertions(+), 112 deletions(-) diff --git a/src/FMData.Rest/FileMakerRestClient.cs b/src/FMData.Rest/FileMakerRestClient.cs index 7889bce..6eead1b 100644 --- a/src/FMData.Rest/FileMakerRestClient.cs +++ b/src/FMData.Rest/FileMakerRestClient.cs @@ -271,11 +271,7 @@ public async Task LogoutAsync() /// public override async Task> FindAsync( string layout, - Dictionary req, - int skip, - int take, - string script, - string scriptParameter) + Dictionary req) { if (string.IsNullOrEmpty(layout)) throw new ArgumentException("Layout is required on the request."); @@ -283,10 +279,6 @@ public override async Task> FindAsync( fmdataRequest.AddQuery(req, false); - fmdataRequest.SetLimit(take).SetOffset(skip); - - fmdataRequest.SetScript(scriptName: script, scriptParameter: scriptParameter); - var response = await ExecuteRequestAsync(HttpMethod.Post, FindEndpoint(layout), fmdataRequest).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.NotFound) @@ -299,6 +291,7 @@ public override async Task> FindAsync( try { var responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var responseObject = JsonConvert.DeserializeObject>(responseJson); return responseObject.Response.Data.Select(d => d.FieldData); @@ -547,20 +540,12 @@ public override async Task SendAsync(IDeleteRequest req) } } - /// - /// Strongly typed find request. - /// - /// The type of response objects to return. - /// The find request parameters. - /// Function to assign the FileMaker RecordId to each instance of {T}. - /// Function to assign the FileMaker ModId to each instance of {T}. - /// Indicates whether the data information portion should be parsed. - /// An matching the request parameters. - public override async Task<(IEnumerable, DataInfoModel)> SendAsync( - IFindRequest req, + /// + public override async Task<(IEnumerable, DataInfoModel)> SendAsync( + IFindRequest req, bool includeDataInfo, - Func fmId = null, - Func modId = null) + Func fmId = null, + Func modId = null) { if (string.IsNullOrEmpty(req.Layout)) throw new ArgumentException("Layout is required on the find request."); @@ -593,7 +578,7 @@ public override async Task SendAsync(IDeleteRequest req) } // serialize JSON results into .NET objects - IList searchResults = new List(); + IList searchResults = new List(); foreach (var result in results) { var searchResult = ConvertJTokenToInstance(fmId, modId, result); @@ -621,7 +606,7 @@ public override async Task SendAsync(IDeleteRequest req) if (responseObject.Messages.Any(m => m.Code == "401")) { // FileMaker no records match the find request => empty list. - return (new List(), new DataInfoModel()); + return (new List(), new DataInfoModel()); } // throw FMDataException for anything not a 401. throw new FMDataException( @@ -633,7 +618,7 @@ public override async Task SendAsync(IDeleteRequest req) // not found, so return empty list if (response.StatusCode == HttpStatusCode.NotFound) { - return (new List(), new DataInfoModel()); + return (new List(), new DataInfoModel()); } // other error diff --git a/src/FMData.Xml/FileMakerXmlClient.cs b/src/FMData.Xml/FileMakerXmlClient.cs index 64feef5..32fd40c 100644 --- a/src/FMData.Xml/FileMakerXmlClient.cs +++ b/src/FMData.Xml/FileMakerXmlClient.cs @@ -108,11 +108,7 @@ public override Task>> SendAsync(IFindR /// public override Task> FindAsync( string layout, - Dictionary req, - int skip, - int take, - string script, - string scriptParameter) + Dictionary req) { throw new NotImplementedException(); } @@ -182,20 +178,12 @@ public override async Task SendAsync(IEditRequest req) throw new Exception("Unable to complete request"); } - /// - /// Executes a Find Request and returns the matching objects projected by the type parameter. - /// - /// The type to project the results against. - /// The Find Request Command. - /// Return the data info portion of the request. - /// The function to map FileMaker Record Ids to an instance of T. - /// The function to map FileMaker modId to an instance of T - /// The projected results matching the find request. - public override async Task<(IEnumerable, DataInfoModel)> SendAsync( - IFindRequest req, + /// + public override async Task<(IEnumerable, DataInfoModel)> SendAsync( + IFindRequest req, bool includeDataInfo, - Func fmId = null, - Func modId = null) + Func fmId = null, + Func modId = null) { var response = await ExecuteRequestAsync(req).ConfigureAwait(false); @@ -230,11 +218,11 @@ public override async Task SendAsync(IEditRequest req) var records = xDocument .Descendants(_ns + "resultset") .Elements(_ns + "record") - .Select(r => new RecordBase>>> + .Select(r => new RecordBase>>> { RecordId = Convert.ToInt32(r.Attribute("record-id").Value), ModId = Convert.ToInt32(r.Attribute("mod-id").Value), - FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject(), + FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject(), PortalData = r.Elements(_ns + "relatedset") .ToDictionary( k => k.Attribute("table").Value, @@ -255,8 +243,8 @@ public override async Task SendAsync(IEditRequest req) fmId?.Invoke(record.FieldData, record.RecordId); modId?.Invoke(record.FieldData, record.ModId); - // TODO: update each record's FieldData instance with the contents of its PortalData - var portals = typeof(T).GetTypeInfo().DeclaredProperties.Where(p => p.GetCustomAttribute() != null); + update each record's FieldData instance with the contents of its PortalData + var portals = typeof(TResponse).GetTypeInfo().DeclaredProperties.Where(p => p.GetCustomAttribute() != null); foreach (var portal in portals) { var portalDataAttr = portal.GetCustomAttribute(); @@ -319,7 +307,6 @@ public override Task SetGlobalFieldAsync(string baseTable, string fie /// /// Upload data to a container field. - /// TODO: Workaround with B64 encoding and container auto-enter? /// public override Task UpdateContainerAsync(string layout, int recordId, string fieldName, string fileName, int repetition, byte[] content) { @@ -400,19 +387,13 @@ public override Task GetLayoutAsync(string layout, int? recordId throw new NotImplementedException(); } - /// - /// - /// - /// + /// public override Task> GetLayoutsAsync() { throw new NotImplementedException(); } - /// - /// - /// - /// + /// public override Task> GetScriptsAsync() { throw new NotImplementedException(); diff --git a/src/FMData/FileMakerApiClientBase.cs b/src/FMData/FileMakerApiClientBase.cs index 17b88ab..f28550f 100644 --- a/src/FMData/FileMakerApiClientBase.cs +++ b/src/FMData/FileMakerApiClientBase.cs @@ -651,45 +651,30 @@ public virtual async Task> SendAsync( Func fmId, Func modId) where T : class, new() { - var (data, info) = await SendAsync(req, false, fmId, modId).ConfigureAwait(false); + var (data, _) = await SendAsync(req, false, fmId, modId).ConfigureAwait(false); return data; } - /// - /// Send a Find Record request to the FileMaker API. - /// - public abstract Task<(IEnumerable, DataInfoModel)> SendAsync( + /// + public virtual async Task<(IEnumerable, DataInfoModel)> SendAsync( IFindRequest req, bool includeDataInfo, Func fmId = null, - Func modId = null) where T : class, new(); - - #endregion + Func modId = null) where T : class, new() + { + return await SendAsync(req, includeDataInfo, fmId, modId).ConfigureAwait(false); + } - /// - /// Find a record with utilizing a class instance to define the find request field values. - /// - /// The response type to extract and return. - /// The layout to perform the request on. - /// The dictionary of key/value pairs to find against. - /// - public virtual Task> FindAsync( - string layout, Dictionary req) => FindAsync( - layout: layout, - req: req, - skip: 0, - take: 100, - script: null, - scriptParameter: null); + /// + public abstract Task<(IEnumerable, DataInfoModel)> SendAsync( + IFindRequest req, + bool includeDataInfo, + Func fmId = null, + Func modId = null) where TResponse : class, new(); + #endregion /// - public abstract Task> FindAsync( - string layout, - Dictionary req, - int skip, - int take, - string script, - string scriptParameter); + public abstract Task> FindAsync(string layout, Dictionary req); /// /// Get a single record by FileMaker RecordId diff --git a/src/FMData/IFileMakerApiClient.cs b/src/FMData/IFileMakerApiClient.cs index 9a03183..18e7bb9 100644 --- a/src/FMData/IFileMakerApiClient.cs +++ b/src/FMData/IFileMakerApiClient.cs @@ -20,7 +20,7 @@ public interface IFileMakerApiClient /// /// The initial find request data. /// The type used for the create request. - /// An IFindRequest{T} instance setup per the initial query paramater. + /// An IFindRequest{T} instance setup per the initial query parameter. ICreateRequest GenerateCreateRequest(T data); /// @@ -33,7 +33,7 @@ public interface IFileMakerApiClient /// /// The initial edit data request. /// The type used for the edit request. - /// An IEditRequest{T} instance setup per the initial query paramater. + /// An IEditRequest{T} instance setup per the initial query parameter. IEditRequest GenerateEditRequest(T data); /// @@ -46,7 +46,7 @@ public interface IFileMakerApiClient /// /// The initial find request data. /// The type used for the find request. - /// An IFindRequest{T} instance setup per the initial query paramater. + /// An IFindRequest{T} instance setup per the initial query parameter. IFindRequest GenerateFindRequest(T initialQuery); /// @@ -56,7 +56,7 @@ public interface IFileMakerApiClient #endregion /// - /// Runs a script with the specified layout context and with an optional (null/empty OK) paramater. + /// Runs a script with the specified layout context and with an optional (null/empty OK) parameter. /// /// The layout to use for the context of the script. /// The name of the script to run. @@ -296,27 +296,6 @@ public interface IFileMakerApiClient /// /// Task> FindAsync(string layout, Dictionary req); - - /// - /// General purpose Find Request method. Supports additional syntaxes like the { "omit" : "true" } operation. - /// This method returns a strongly typed but accepts a the more flexible request parameters. - /// - /// the type of response objects to return. - /// The layout to perform the find request on. - /// The find request dictionary. - /// The number of records to skip. - /// The number of records to return. - /// The name of a script to run (or null) - /// The script parameter value (or null) - /// An matching the request parameters. - /// Can't be a relay method, since we have to process the data specially to get our output - Task> FindAsync( - string layout, - Dictionary req, - int skip, - int take, - string script, - string scriptParameter); #endregion #region Edit @@ -374,7 +353,7 @@ Task> FindAsync( /// /// Delete a record by FileMaker RecordId. /// - /// The filemaker RecordId to delete. + /// The FileMaker RecordId to delete. /// Used to pull the [TableAttribute] value to determine the layout to use. /// /// Use the other delete overload if the class does not use the [Table] attribute. @@ -509,6 +488,23 @@ Task> SendAsync( Func fmId = null, Func modId = null) where T : class, new(); + /// + /// Find a record or records matching the request and include a data info model as well as the response. + /// + /// The Response type. + /// The Request type. + /// The find request parameters. + /// Function to assign the FileMaker RecordId to each instance of {T}. + /// Function to assign the FileMaker ModId to each instance of {T}. + /// Indicates whether the data information portion should be parsed. + /// An matching the request parameters. + /// The data info portion of the response is always returned when correctly parsed. + Task<(IEnumerable, DataInfoModel)> SendAsync( + IFindRequest req, + bool includeDataInfo, + Func fmId = null, + Func modId = null) where TResponse : class, new(); + /// /// Edit record. /// diff --git a/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs b/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs index ac0f1b5..bca5120 100644 --- a/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs +++ b/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs @@ -223,6 +223,35 @@ public async Task SendAsync_Find_Should_Have_DataInfo() Assert.Equal(123, info.FoundCount); } + [Fact] + public async Task SendAsync_Using_Dictionary_Find_Should_Have_DataInfo() + { + // arrange + var mockHttp = new MockHttpMessageHandler(); + + var layout = "the-layout"; + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find") + .Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo()); + + var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection); + + var toFind = new Dictionary() { { "Id", "35" } }; + var req = new FindRequest>() { Layout = layout }; + req.AddQuery(toFind, false); + + // act + var (data, info) = await fdc.SendAsync>(req, true); + + // assert + Assert.NotEmpty(data); + Assert.Equal(1, info.ReturnedCount); + Assert.Equal(123, info.FoundCount); + } + [Fact] public async Task SendAsyncFind_WithOmit_Omits() {