Skip to content

Commit 2e6eadc

Browse files
authored
feat: enable portal limit and offset parameters on find requests (#414)
Fixes #214.
1 parent 4d49f15 commit 2e6eadc

16 files changed

Lines changed: 804 additions & 19 deletions

File tree

.github/workflows/linter.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
uses: github/super-linter@v7
4848
env:
4949
VALIDATE_ALL_CODEBASE: false
50+
VALIDATE_MARKDOWN_PRETTIER: false
5051
FILTER_REGEX_EXCLUDE: README.md
5152
DEFAULT_BRANCH: master
5253
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ services.AddSingleton<IFileMakerApiClient, FileMakerRestClient>(s => {
117117
});
118118
```
119119

120-
Behind the scenes, the injected `HttpClient` is kept alive for the lifetime of the FMData client (rest/xml) and reused throughout. This is useful to manage the lifetime of `IFileMakerApiClient` as a singleton, since it stores data about FileMaker Data API tokens and reuses them as much as possible. Simply using `services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();` keeps the lifetime of our similar to that of a 'managed `HttpClient`' which works for simple scenarios.
120+
Behind the scenes, the injected `HttpClient` is kept alive for the lifetime of the FMData client (rest/xml) and reused throughout. This is useful to manage the lifetime of `IFileMakerApiClient` as a singleton, since it stores data about FileMaker Data API tokens and reuses them as much as possible. Simply using `services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();` keeps the lifetime of our similar to that of a 'managed `HttpClient`' which works for simple scenarios.
121121

122122
Test both approaches in your solution and use what works.
123123

@@ -192,6 +192,43 @@ var (data, info) = await fdc.SendAsync(req, true);
192192

193193
Alternatively, if you create a calculated field `Get(RecordID)` and put it on your layout then map it the normal way.
194194

195+
### Find with Portal Limit and Offset
196+
197+
By default, the FileMaker Data API limits portal records to 50 per request. You can control per-portal `limit` and `offset` using the fluent `WithPortal` builder or `ConfigurePortal` method on `FindRequest<T>`.
198+
199+
Use the fluent builder to chain portal configuration:
200+
201+
```csharp
202+
var req = new FindRequest<Model>() { Layout = "layout" };
203+
req.AddQuery(new Model { Name = "someName" }, false);
204+
205+
// configure portals with limit and offset
206+
req.WithPortal("RelatedInvoices").Limit(100).Offset(1)
207+
.WithPortal("LineItems").Limit(200);
208+
209+
var results = await client.SendAsync(req);
210+
```
211+
212+
Or use `ConfigurePortal` directly:
213+
214+
```csharp
215+
var req = new FindRequest<Model>() { Layout = "layout" };
216+
req.AddQuery(new Model { Name = "someName" }, false);
217+
req.ConfigurePortal("RelatedInvoices", limit: 100, offset: 1);
218+
var results = await client.SendAsync(req);
219+
```
220+
221+
To include specific portals in the response without setting limits:
222+
223+
```csharp
224+
var req = new FindRequest<Model>() { Layout = "layout" };
225+
req.AddQuery(new Model { Name = "someName" }, false);
226+
req.IncludePortals("RelatedInvoices", "LineItems");
227+
var results = await client.SendAsync(req);
228+
```
229+
230+
Portal parameters work with both find requests (POST to `_find`) and empty-query get-records requests (GET).
231+
195232
### Find and load Container Data
196233

197234
Make sure you use the `[ContainerDataFor("NameOfContainer")]` attribute along with a `byte[]` property for processing of your model.

docs/example-use.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ services.AddSingleton<IFileMakerApiClient, FileMakerRestClient>(s => {
7373
});
7474
```
7575

76-
Behind the scenes, the injected `HttpClient` is kept alive for the lifetime of the FMData client (rest/xml) and reused throughout. This is useful to manage the lifetime of `IFileMakerApiClient` as a singleton, since it stores data about FileMaker Data API tokens and reuses them as much as possible. Simply using `services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();` keeps the lifetime of our similar to that of a 'managed `HttpClient`' which works for simple scenarios.
76+
Behind the scenes, the injected `HttpClient` is kept alive for the lifetime of the FMData client (rest/XML) and reused throughout. This is useful to manage the lifetime of `IFileMakerApiClient` as a singleton, since it stores data about FileMaker Data API tokens and reuses them as much as possible. Simply using `services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();` keeps the lifetime of our similar to that of a 'managed `HttpClient`' which works for simple scenarios.
7777

7878
Test both approaches in your solution and use what works.
7979

@@ -148,6 +148,43 @@ var (data, info) = await fdc.SendAsync(req, true);
148148

149149
Alternatively, if you create a calculated field `Get(RecordID)` and put it on your layout then map it the normal way.
150150

151+
### Find with Portal Limit and Offset
152+
153+
By default, the FileMaker Data API limits portal records to 50 per request. You can control per-portal `limit` and `offset` using the fluent `WithPortal` builder or `ConfigurePortal` method on `FindRequest<T>`.
154+
155+
Use the fluent builder to chain portal configuration:
156+
157+
```csharp
158+
var req = new FindRequest<Model>() { Layout = "layout" };
159+
req.AddQuery(new Model { Name = "someName" }, false);
160+
161+
// configure portals with limit and offset
162+
req.WithPortal("RelatedInvoices").Limit(100).Offset(1)
163+
.WithPortal("LineItems").Limit(200);
164+
165+
var results = await client.SendAsync(req);
166+
```
167+
168+
Or use `ConfigurePortal` directly:
169+
170+
```csharp
171+
var req = new FindRequest<Model>() { Layout = "layout" };
172+
req.AddQuery(new Model { Name = "someName" }, false);
173+
req.ConfigurePortal("RelatedInvoices", limit: 100, offset: 1);
174+
var results = await client.SendAsync(req);
175+
```
176+
177+
To include specific portals in the response without setting limits:
178+
179+
```csharp
180+
var req = new FindRequest<Model>() { Layout = "layout" };
181+
req.AddQuery(new Model { Name = "someName" }, false);
182+
req.IncludePortals("RelatedInvoices", "LineItems");
183+
var results = await client.SendAsync(req);
184+
```
185+
186+
Portal parameters work with both find requests (POST to `_find`) and empty-query get-records requests (GET).
187+
151188
### Find and load Container Data
152189

153190
Make sure you use the `[ContainerDataFor("NameOfContainer")]` attribute along with a `byte[]` property for processing of your model.

src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</ItemGroup>
2828

2929
<PropertyGroup>
30-
<MinVerMinimumMajorMinor>5.2</MinVerMinimumMajorMinor>
30+
<MinVerMinimumMajorMinor>6.0</MinVerMinimumMajorMinor>
3131
<MinVerTagPrefix>v</MinVerTagPrefix>
3232
<MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
3333
</PropertyGroup>

src/FMData.Rest/FMData.Rest.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</ItemGroup>
2828

2929
<PropertyGroup>
30-
<MinVerMinimumMajorMinor>5.2</MinVerMinimumMajorMinor>
30+
<MinVerMinimumMajorMinor>6.0</MinVerMinimumMajorMinor>
3131
<MinVerTagPrefix>v</MinVerTagPrefix>
3232
<MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
3333
</PropertyGroup>

src/FMData.Rest/FileMakerRestClient.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,31 @@ public FileMakerRestClient(
174174
/// <returns>The FileMaker Data API Endpoint for Get Records requests.</returns>
175175
public string GetRecordsEndpoint(string layout, int limit, int offset) => $"{BaseEndPoint}/layouts/{Uri.EscapeDataString(layout)}/records?_limit={limit}&_offset={offset}";
176176

177+
/// <inheritdoc />
178+
public string GetRecordsEndpoint(string layout, int limit, int offset, ICollection<PortalRequestData> portals)
179+
{
180+
var url = $"{BaseEndPoint}/layouts/{Uri.EscapeDataString(layout)}/records?_limit={limit}&_offset={offset}";
181+
182+
if (portals != null && portals.Count > 0)
183+
{
184+
url += $"&portal={Uri.EscapeDataString($"[{string.Join(",", portals.Select(p => $"\"{p.PortalName}\""))}]")}";
185+
186+
foreach (var portal in portals)
187+
{
188+
if (portal.Limit.HasValue)
189+
{
190+
url += $"&_limit.{Uri.EscapeDataString(portal.PortalName)}={portal.Limit.Value}";
191+
}
192+
if (portal.Offset.HasValue)
193+
{
194+
url += $"&_offset.{Uri.EscapeDataString(portal.PortalName)}={portal.Offset.Value}";
195+
}
196+
}
197+
}
198+
199+
return url;
200+
}
201+
177202
/// <summary>
178203
/// Generate the appropriate Edit/Update endpoint uri for this instance of the data client.
179204
/// </summary>
@@ -575,7 +600,7 @@ public override async Task<IResponse> SendAsync(IDeleteRequest req)
575600
if (req.Query == null || req.Query.Count() == 0)
576601
{
577602
// if this is an empty query, just punch it in to the Records API instead of the Find API.
578-
uri = GetRecordsEndpoint(req.Layout, req.Limit, req.Offset);
603+
uri = GetRecordsEndpoint(req.Layout, req.Limit, req.Offset, req.Portals);
579604
method = HttpMethod.Get;
580605
}
581606

src/FMData.Rest/IFileMakerRestClient.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Net.Http;
1+
using System.Collections.Generic;
2+
using System.Net.Http;
23
using System.Threading.Tasks;
34
using FMData.Rest.Responses;
45

@@ -54,6 +55,16 @@ public interface IFileMakerRestClient : IFileMakerApiClient
5455
/// <returns>The FileMaker Data API Endpoint for Get Records requests.</returns>
5556
string GetRecordsEndpoint(string layout, int range, int offset);
5657

58+
/// <summary>
59+
/// Generate the appropriate Get Records endpoint with per-portal parameters.
60+
/// </summary>
61+
/// <param name="layout">The layout to use as the context for the response.</param>
62+
/// <param name="range">The number of records to return.</param>
63+
/// <param name="offset">The offset number of records to skip before starting to return records.</param>
64+
/// <param name="portals">The portal configurations to include in the request.</param>
65+
/// <returns>The FileMaker Data API Endpoint for Get Records requests.</returns>
66+
string GetRecordsEndpoint(string layout, int range, int offset, ICollection<PortalRequestData> portals);
67+
5768
/// <summary>
5869
/// Generate the appropriate Create endpoint uri for this instance of the data client.
5970
/// </summary>

src/FMData.Rest/Requests/FindRequest.cs

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Collections.Generic;
2+
using System.Linq;
23
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
35

46
namespace FMData.Rest.Requests
57
{
@@ -20,7 +22,7 @@ public partial class FindRequest<TRequestType> : RequestBase, IFindRequest<TRequ
2022
/// Maximum number of records to return for this request.
2123
/// </summary>
2224
[JsonProperty("limit")]
23-
public int Limit { get; set; } = 100; // default
25+
public int Limit { get; set; } = 100; // default
2426

2527
/// <summary>
2628
/// The number of records to skip before returning records for this request.
@@ -39,6 +41,51 @@ public partial class FindRequest<TRequestType> : RequestBase, IFindRequest<TRequ
3941
/// </summary>
4042
public bool LoadContainerData { get; set; }
4143

44+
/// <summary>
45+
/// The portal configurations for this request.
46+
/// </summary>
47+
[JsonIgnore]
48+
public ICollection<PortalRequestData> Portals { get; set; }
49+
50+
/// <summary>
51+
/// Extension data dictionary used to inject portal dot-notation keys
52+
/// (e.g., "portal", "limit.PortalName", "offset.PortalName") into the
53+
/// serialized JSON output.
54+
/// </summary>
55+
[JsonExtensionData]
56+
private IDictionary<string, JToken> _portalExtensionData;
57+
58+
/// <summary>
59+
/// Configure a portal's limit and/or offset parameters.
60+
/// If the portal has already been configured, updates its values.
61+
/// </summary>
62+
/// <param name="portalName">The name of the portal (table occurrence).</param>
63+
/// <param name="limit">The maximum number of portal records to return.</param>
64+
/// <param name="offset">The number of portal records to skip.</param>
65+
public void ConfigurePortal(string portalName, int? limit = null, int? offset = null)
66+
{
67+
if (Portals == null)
68+
{
69+
Portals = new List<PortalRequestData>();
70+
}
71+
72+
var existing = Portals.FirstOrDefault(p => p.PortalName == portalName);
73+
if (existing != null)
74+
{
75+
if (limit.HasValue) existing.Limit = limit;
76+
if (offset.HasValue) existing.Offset = offset;
77+
}
78+
else
79+
{
80+
Portals.Add(new PortalRequestData
81+
{
82+
PortalName = portalName,
83+
Limit = limit,
84+
Offset = offset
85+
});
86+
}
87+
}
88+
4289
/// <summary>
4390
/// Create a find request from Json
4491
/// </summary>
@@ -50,18 +97,54 @@ public partial class FindRequest<TRequestType> : RequestBase, IFindRequest<TRequ
5097
/// JSON Convert the current object to a string for passing out to the API.
5198
/// </summary>
5299
/// <returns></returns>
53-
public override string SerializeRequest() => JsonConvert.SerializeObject(this,
54-
Formatting.None,
55-
new JsonSerializerSettings
100+
public override string SerializeRequest()
101+
{
102+
PopulatePortalExtensionData();
103+
104+
return JsonConvert.SerializeObject(this,
105+
Formatting.None,
106+
new JsonSerializerSettings
107+
{
108+
NullValueHandling = IncludeNullValuesInSerializedOutput ? NullValueHandling.Include : NullValueHandling.Ignore,
109+
DefaultValueHandling = IncludeDefaultValuesInSerializedOutput ? DefaultValueHandling.Include : DefaultValueHandling.Ignore,
110+
Converters =
111+
{
112+
new FormatNumbersAsTextConverter(),
113+
new RequestQueryInstanceConverter<TRequestType>(this)
114+
}
115+
});
116+
}
117+
118+
/// <summary>
119+
/// Populates the extension data dictionary from the Portals collection
120+
/// so that dot-notation keys appear as top-level JSON properties.
121+
/// </summary>
122+
private void PopulatePortalExtensionData()
123+
{
124+
_portalExtensionData = null;
125+
126+
if (Portals == null || Portals.Count == 0)
56127
{
57-
NullValueHandling = IncludeNullValuesInSerializedOutput ? NullValueHandling.Include : NullValueHandling.Ignore,
58-
DefaultValueHandling = IncludeDefaultValuesInSerializedOutput ? DefaultValueHandling.Include : DefaultValueHandling.Ignore,
59-
Converters =
128+
return;
129+
}
130+
131+
_portalExtensionData = new Dictionary<string, JToken>();
132+
133+
var portalNames = Portals.Select(p => p.PortalName).ToList();
134+
_portalExtensionData["portal"] = JToken.FromObject(portalNames);
135+
136+
foreach (var portal in Portals)
137+
{
138+
if (portal.Limit.HasValue)
60139
{
61-
new FormatNumbersAsTextConverter(),
62-
new RequestQueryInstanceConverter<TRequestType>(this)
140+
_portalExtensionData[$"limit.{portal.PortalName}"] = portal.Limit.Value;
63141
}
64-
});
142+
if (portal.Offset.HasValue)
143+
{
144+
_portalExtensionData[$"offset.{portal.PortalName}"] = portal.Offset.Value;
145+
}
146+
}
147+
}
65148

66149
/// <summary>
67150
/// Add an instance to the query collection.

src/FMData.Xml/FMData.Xml.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</ItemGroup>
2828

2929
<PropertyGroup>
30-
<MinVerMinimumMajorMinor>5.2</MinVerMinimumMajorMinor>
30+
<MinVerMinimumMajorMinor>6.0</MinVerMinimumMajorMinor>
3131
<MinVerTagPrefix>v</MinVerTagPrefix>
3232
<MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
3333
</PropertyGroup>

src/FMData.Xml/Requests/FindRequest.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ public class FindRequest<T> : RequestBase, IFindRequest<T>
3636
/// </summary>
3737
public bool LoadContainerData { get; set; }
3838

39+
/// <summary>
40+
/// Portal configurations. Not supported by the XML API; included for interface compatibility.
41+
/// </summary>
42+
public ICollection<PortalRequestData> Portals { get; set; }
43+
44+
/// <summary>
45+
/// Configure a portal. Not supported by the XML API; this is a no-op stub for interface compatibility.
46+
/// </summary>
47+
public void ConfigurePortal(string portalName, int? limit = null, int? offset = null) { }
48+
3949
/// <summary>
4050
/// Serialize the request.
4151
/// </summary>

0 commit comments

Comments
 (0)