Skip to content

Commit

Permalink
feat: implement SendAsync (find) with different query/response generi…
Browse files Browse the repository at this point in the history
…c types
  • Loading branch information
fuzzzerd authored Nov 8, 2023
1 parent a5e2a47 commit 6e558a1
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 112 deletions.
35 changes: 10 additions & 25 deletions src/FMData.Rest/FileMakerRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,22 +271,14 @@ public async Task<IResponse> LogoutAsync()
/// <inheritdoc />
public override async Task<IEnumerable<T>> FindAsync<T>(
string layout,
Dictionary<string, string> req,
int skip,
int take,
string script,
string scriptParameter)
Dictionary<string, string> req)
{
if (string.IsNullOrEmpty(layout)) throw new ArgumentException("Layout is required on the request.");

var fmdataRequest = new FindRequest<Dictionary<string, string>> { Layout = layout };

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)
Expand All @@ -299,6 +291,7 @@ public override async Task<IEnumerable<T>> FindAsync<T>(
try
{
var responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

var responseObject = JsonConvert.DeserializeObject<FindResponse<T>>(responseJson);

return responseObject.Response.Data.Select(d => d.FieldData);
Expand Down Expand Up @@ -547,20 +540,12 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
}
}

/// <summary>
/// Strongly typed find request.
/// </summary>
/// <typeparam name="T">The type of response objects to return.</typeparam>
/// <param name="req">The find request parameters.</param>
/// <param name="fmId">Function to assign the FileMaker RecordId to each instance of {T}.</param>
/// <param name="modId">Function to assign the FileMaker ModId to each instance of {T}.</param>
/// <param name="includeDataInfo">Indicates whether the data information portion should be parsed.</param>
/// <returns>An <see cref="IEnumerable{T}"/> matching the request parameters.</returns>
public override async Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
IFindRequest<T> req,
/// <inheritdoc />
public override async Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null)
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null)
{
if (string.IsNullOrEmpty(req.Layout)) throw new ArgumentException("Layout is required on the find request.");

Expand Down Expand Up @@ -593,7 +578,7 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
}

// serialize JSON results into .NET objects
IList<T> searchResults = new List<T>();
IList<TResponse> searchResults = new List<TResponse>();
foreach (var result in results)
{
var searchResult = ConvertJTokenToInstance(fmId, modId, result);
Expand Down Expand Up @@ -621,7 +606,7 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
if (responseObject.Messages.Any(m => m.Code == "401"))
{
// FileMaker no records match the find request => empty list.
return (new List<T>(), new DataInfoModel());
return (new List<TResponse>(), new DataInfoModel());
}
// throw FMDataException for anything not a 401.
throw new FMDataException(
Expand All @@ -633,7 +618,7 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
// not found, so return empty list
if (response.StatusCode == HttpStatusCode.NotFound)
{
return (new List<T>(), new DataInfoModel());
return (new List<TResponse>(), new DataInfoModel());
}

// other error
Expand Down
43 changes: 12 additions & 31 deletions src/FMData.Xml/FileMakerXmlClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,7 @@ public override Task<IFindResponse<Dictionary<string, string>>> SendAsync(IFindR
/// <inheritdoc />
public override Task<IEnumerable<T>> FindAsync<T>(
string layout,
Dictionary<string, string> req,
int skip,
int take,
string script,
string scriptParameter)
Dictionary<string, string> req)
{
throw new NotImplementedException();
}
Expand Down Expand Up @@ -182,20 +178,12 @@ public override async Task<IEditResponse> SendAsync<T>(IEditRequest<T> req)
throw new Exception("Unable to complete request");
}

/// <summary>
/// Executes a Find Request and returns the matching objects projected by the type parameter.
/// </summary>
/// <typeparam name="T">The type to project the results against.</typeparam>
/// <param name="req">The Find Request Command.</param>
/// <param name="includeDataInfo">Return the data info portion of the request.</param>
/// <param name="fmId">The function to map FileMaker Record Ids to an instance of T.</param>
/// <param name="modId">The function to map FileMaker modId to an instance of T</param>
/// <returns>The projected results matching the find request.</returns>
public override async Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
IFindRequest<T> req,
/// <inheritdoc />
public override async Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null)
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null)
{
var response = await ExecuteRequestAsync(req).ConfigureAwait(false);

Expand Down Expand Up @@ -230,11 +218,11 @@ public override async Task<IEditResponse> SendAsync<T>(IEditRequest<T> req)
var records = xDocument
.Descendants(_ns + "resultset")
.Elements(_ns + "record")
.Select(r => new RecordBase<T, Dictionary<string, IEnumerable<Dictionary<string, object>>>>
.Select(r => new RecordBase<TResponse, Dictionary<string, IEnumerable<Dictionary<string, object>>>>
{
RecordId = Convert.ToInt32(r.Attribute("record-id").Value),
ModId = Convert.ToInt32(r.Attribute("mod-id").Value),
FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject<T>(),
FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject<TResponse>(),
PortalData = r.Elements(_ns + "relatedset")
.ToDictionary(
k => k.Attribute("table").Value,
Expand All @@ -255,8 +243,8 @@ public override async Task<IEditResponse> SendAsync<T>(IEditRequest<T> 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<PortalDataAttribute>() != null);
update each record's FieldData instance with the contents of its PortalData
var portals = typeof(TResponse).GetTypeInfo().DeclaredProperties.Where(p => p.GetCustomAttribute<PortalDataAttribute>() != null);
foreach (var portal in portals)
{
var portalDataAttr = portal.GetCustomAttribute<PortalDataAttribute>();
Expand Down Expand Up @@ -319,7 +307,6 @@ public override Task<IResponse> SetGlobalFieldAsync(string baseTable, string fie

/// <summary>
/// Upload data to a container field.
/// TODO: Workaround with B64 encoding and container auto-enter?
/// </summary>
public override Task<IEditResponse> UpdateContainerAsync(string layout, int recordId, string fieldName, string fileName, int repetition, byte[] content)
{
Expand Down Expand Up @@ -400,19 +387,13 @@ public override Task<LayoutMetadata> GetLayoutAsync(string layout, int? recordId
throw new NotImplementedException();
}

/// <summary>
///
/// </summary>
/// <returns></returns>
/// <inheritdoc />
public override Task<IReadOnlyCollection<LayoutListItem>> GetLayoutsAsync()
{
throw new NotImplementedException();
}

/// <summary>
///
/// </summary>
/// <returns></returns>
/// <inheritdoc />
public override Task<IReadOnlyCollection<ScriptListItem>> GetScriptsAsync()
{
throw new NotImplementedException();
Expand Down
45 changes: 15 additions & 30 deletions src/FMData/FileMakerApiClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -651,45 +651,30 @@ public virtual async Task<IEnumerable<T>> SendAsync<T>(
Func<T, int, object> fmId,
Func<T, int, object> modId) where T : class, new()
{
var (data, info) = await SendAsync(req, false, fmId, modId).ConfigureAwait(false);
var (data, _) = await SendAsync<T, T>(req, false, fmId, modId).ConfigureAwait(false);
return data;
}

/// <summary>
/// Send a Find Record request to the FileMaker API.
/// </summary>
public abstract Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
/// <inheritdoc />
public virtual async Task<(IEnumerable<T>, DataInfoModel)> SendAsync<T>(
IFindRequest<T> req,
bool includeDataInfo,
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null) where T : class, new();

#endregion
Func<T, int, object> modId = null) where T : class, new()
{
return await SendAsync<T, T>(req, includeDataInfo, fmId, modId).ConfigureAwait(false);
}

/// <summary>
/// Find a record with utilizing a class instance to define the find request field values.
/// </summary>
/// <typeparam name="T">The response type to extract and return.</typeparam>
/// <param name="layout">The layout to perform the request on.</param>
/// <param name="req">The dictionary of key/value pairs to find against.</param>
/// <returns></returns>
public virtual Task<IEnumerable<T>> FindAsync<T>(
string layout, Dictionary<string, string> req) => FindAsync<T>(
layout: layout,
req: req,
skip: 0,
take: 100,
script: null,
scriptParameter: null);
/// <inheritdoc />
public abstract Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null) where TResponse : class, new();

#endregion
/// <inheritdoc />
public abstract Task<IEnumerable<T>> FindAsync<T>(
string layout,
Dictionary<string, string> req,
int skip,
int take,
string script,
string scriptParameter);
public abstract Task<IEnumerable<T>> FindAsync<T>(string layout, Dictionary<string, string> req);

/// <summary>
/// Get a single record by FileMaker RecordId
Expand Down
48 changes: 22 additions & 26 deletions src/FMData/IFileMakerApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public interface IFileMakerApiClient
/// </summary>
/// <param name="data">The initial find request data.</param>
/// <typeparam name="T">The type used for the create request.</typeparam>
/// <returns>An IFindRequest{T} instance setup per the initial query paramater.</returns>
/// <returns>An IFindRequest{T} instance setup per the initial query parameter.</returns>
ICreateRequest<T> GenerateCreateRequest<T>(T data);

/// <summary>
Expand All @@ -33,7 +33,7 @@ public interface IFileMakerApiClient
/// </summary>
/// <param name="data">The initial edit data request.</param>
/// <typeparam name="T">The type used for the edit request.</typeparam>
/// <returns>An IEditRequest{T} instance setup per the initial query paramater.</returns>
/// <returns>An IEditRequest{T} instance setup per the initial query parameter.</returns>
IEditRequest<T> GenerateEditRequest<T>(T data);

/// <summary>
Expand All @@ -46,7 +46,7 @@ public interface IFileMakerApiClient
/// </summary>
/// <param name="initialQuery">The initial find request data.</param>
/// <typeparam name="T">The type used for the find request.</typeparam>
/// <returns>An IFindRequest{T} instance setup per the initial query paramater.</returns>
/// <returns>An IFindRequest{T} instance setup per the initial query parameter.</returns>
IFindRequest<T> GenerateFindRequest<T>(T initialQuery);

/// <summary>
Expand All @@ -56,7 +56,7 @@ public interface IFileMakerApiClient
#endregion

/// <summary>
/// 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.
/// </summary>
/// <param name="layout">The layout to use for the context of the script.</param>
/// <param name="script">The name of the script to run.</param>
Expand Down Expand Up @@ -296,27 +296,6 @@ public interface IFileMakerApiClient
/// <param name="req"></param>
/// <returns></returns>
Task<IEnumerable<T>> FindAsync<T>(string layout, Dictionary<string, string> req);

/// <summary>
/// General purpose Find Request method. Supports additional syntaxes like the { "omit" : "true" } operation.
/// This method returns a strongly typed <see cref="IEnumerable{T}"/> but accepts a the more flexible <see cref="Dictionary{TKey, TValue}"/> request parameters.
/// </summary>
/// <typeparam name="T">the type of response objects to return.</typeparam>
/// <param name="layout">The layout to perform the find request on.</param>
/// <param name="req">The find request dictionary.</param>
/// <param name="skip">The number of records to skip.</param>
/// <param name="take">The number of records to return.</param>
/// <param name="script">The name of a script to run (or null)</param>
/// <param name="scriptParameter">The script parameter value (or null)</param>
/// <returns>An <see cref="IEnumerable{T}"/> matching the request parameters.</returns>
/// <remarks>Can't be a relay method, since we have to process the data specially to get our output</remarks>
Task<IEnumerable<T>> FindAsync<T>(
string layout,
Dictionary<string, string> req,
int skip,
int take,
string script,
string scriptParameter);
#endregion

#region Edit
Expand Down Expand Up @@ -374,7 +353,7 @@ Task<IEnumerable<T>> FindAsync<T>(
/// <summary>
/// Delete a record by FileMaker RecordId.
/// </summary>
/// <param name="recId">The filemaker RecordId to delete.</param>
/// <param name="recId">The FileMaker RecordId to delete.</param>
/// <typeparam name="T">Used to pull the [TableAttribute] value to determine the layout to use.</typeparam>
/// <returns></returns>
/// <remarks>Use the other delete overload if the class does not use the [Table] attribute.</remarks>
Expand Down Expand Up @@ -509,6 +488,23 @@ Task<IEnumerable<T>> SendAsync<T>(
Func<T, int, object> fmId = null,
Func<T, int, object> modId = null) where T : class, new();

/// <summary>
/// Find a record or records matching the request and include a data info model as well as the response.
/// </summary>
/// <typeparam name="TResponse">The Response type.</typeparam>
/// <typeparam name="TRequest">The Request type.</typeparam>
/// <param name="req">The find request parameters.</param>
/// <param name="fmId">Function to assign the FileMaker RecordId to each instance of {T}.</param>
/// <param name="modId">Function to assign the FileMaker ModId to each instance of {T}.</param>
/// <param name="includeDataInfo">Indicates whether the data information portion should be parsed.</param>
/// <returns>An <see cref="IEnumerable{T}"/> matching the request parameters.</returns>
/// <remarks>The data info portion of the response is always returned when correctly parsed.</remarks>
Task<(IEnumerable<TResponse>, DataInfoModel)> SendAsync<TResponse, TRequest>(
IFindRequest<TRequest> req,
bool includeDataInfo,
Func<TResponse, int, object> fmId = null,
Func<TResponse, int, object> modId = null) where TResponse : class, new();

/// <summary>
/// Edit record.
/// </summary>
Expand Down
29 changes: 29 additions & 0 deletions tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>() { { "Id", "35" } };
var req = new FindRequest<Dictionary<string, string>>() { Layout = layout };
req.AddQuery(toFind, false);

// act
var (data, info) = await fdc.SendAsync<User, Dictionary<string, string>>(req, true);

// assert
Assert.NotEmpty(data);
Assert.Equal(1, info.ReturnedCount);
Assert.Equal(123, info.FoundCount);
}

[Fact]
public async Task SendAsyncFind_WithOmit_Omits()
{
Expand Down

0 comments on commit 6e558a1

Please sign in to comment.