|
8 | 8 | using Azure.DataApiBuilder.Core.Configurations; |
9 | 9 | using Azure.DataApiBuilder.Core.Models; |
10 | 10 | using Azure.DataApiBuilder.Core.Services; |
| 11 | +using Azure.DataApiBuilder.Service.Exceptions; |
11 | 12 | using HotChocolate.Resolvers; |
12 | 13 | using Microsoft.AspNetCore.Http; |
13 | 14 | using Microsoft.AspNetCore.Http.Extensions; |
@@ -186,22 +187,45 @@ private OkObjectResult FormatFindResult(JsonDocument jsonDoc, FindRequestContext |
186 | 187 | { |
187 | 188 | JsonElement jsonElement = jsonDoc.RootElement.Clone(); |
188 | 189 |
|
| 190 | + // When there are no rows returned from the database, the jsonElement will be an empty array. |
| 191 | + // In that case, the response is returned as is. |
| 192 | + if (jsonElement.ValueKind is JsonValueKind.Array && jsonElement.GetArrayLength() == 0) |
| 193 | + { |
| 194 | + return OkResponse(jsonElement); |
| 195 | + } |
| 196 | + |
| 197 | + HashSet<string> extraFieldsInResponse = (jsonElement.ValueKind is not JsonValueKind.Array) |
| 198 | + ? DetermineExtraFieldsInResponse(jsonElement, context) |
| 199 | + : DetermineExtraFieldsInResponse(jsonElement.EnumerateArray().First(), context); |
| 200 | + |
189 | 201 | // If the results are not a collection or if the query does not have a next page |
190 | | - // no nextLink is needed, return JsonDocument as is |
| 202 | + // no nextLink is needed. So, the response is returned after removing the extra fields. |
191 | 203 | if (jsonElement.ValueKind is not JsonValueKind.Array || !SqlPaginationUtil.HasNext(jsonElement, context.First)) |
192 | 204 | { |
193 | | - // Clones the root element to a new JsonElement that can be |
194 | | - // safely stored beyond the lifetime of the original JsonDocument. |
195 | | - return OkResponse(jsonElement); |
| 205 | + // If there are no additional fields present, the response is returned directly. When there |
| 206 | + // are extra fields, they are removed before returning the response. |
| 207 | + if (extraFieldsInResponse.Count == 0) |
| 208 | + { |
| 209 | + return OkResponse(jsonElement); |
| 210 | + } |
| 211 | + else |
| 212 | + { |
| 213 | + return jsonElement.ValueKind is JsonValueKind.Array ? OkResponse(JsonSerializer.SerializeToElement(RemoveExtraFieldsInResponseWithMultipleItems(jsonElement.EnumerateArray().ToList(), extraFieldsInResponse))) |
| 214 | + : OkResponse(RemoveExtraFieldsInResponseWithSingleItem(jsonElement, extraFieldsInResponse)); |
| 215 | + } |
196 | 216 | } |
197 | 217 |
|
| 218 | + List<JsonElement> rootEnumerated = jsonElement.EnumerateArray().ToList(); |
| 219 | + |
198 | 220 | // More records exist than requested, we know this by requesting 1 extra record, |
199 | 221 | // that extra record is removed here. |
200 | | - IEnumerable<JsonElement> rootEnumerated = jsonElement.EnumerateArray(); |
| 222 | + rootEnumerated.RemoveAt(rootEnumerated.Count - 1); |
201 | 223 |
|
202 | | - rootEnumerated = rootEnumerated.Take(rootEnumerated.Count() - 1); |
| 224 | + // The fields such as primary keys, fields in $orderby clause that are retrieved in addition to the |
| 225 | + // fields requested in the $select clause are required for calculating the $after element which is part of nextLink. |
| 226 | + // So, the extra fields are removed post the calculation of $after element. |
203 | 227 | string after = SqlPaginationUtil.MakeCursorFromJsonElement( |
204 | | - element: rootEnumerated.Last(), |
| 228 | + element: rootEnumerated[rootEnumerated.Count - 1], |
205 | 229 | orderByColumns: context.OrderByClauseOfBackingColumns, |
206 | 230 | primaryKey: _sqlMetadataProvider.GetSourceDefinition(context.EntityName).PrimaryKey, |
207 | 231 | entityName: context.EntityName, |
@@ -233,10 +257,86 @@ private OkObjectResult FormatFindResult(JsonDocument jsonDoc, FindRequestContext |
233 | 257 | path, |
234 | 258 | nvc: context!.ParsedQueryString, |
235 | 259 | after); |
236 | | - rootEnumerated = rootEnumerated.Append(nextLink); |
| 260 | + |
| 261 | + // When there are extra fields present, they are removed before returning the response. |
| 262 | + if (extraFieldsInResponse.Count > 0) |
| 263 | + { |
| 264 | + rootEnumerated = RemoveExtraFieldsInResponseWithMultipleItems(rootEnumerated, extraFieldsInResponse); |
| 265 | + } |
| 266 | + |
| 267 | + rootEnumerated.Add(nextLink); |
237 | 268 | return OkResponse(JsonSerializer.SerializeToElement(rootEnumerated)); |
238 | 269 | } |
239 | 270 |
|
| 271 | + /// <summary> |
| 272 | + /// To support pagination and $first clause with Find requests, it is necessary to provide the nextLink |
| 273 | + /// field in the response. For the calculation of nextLink, the fields such as primary keys, fields in $orderby clause |
| 274 | + /// are retrieved from the database in addition to the fields requested in the $select clause. |
| 275 | + /// However, these fields are not required in the response. |
| 276 | + /// This function helps to determine those additional fields that are present in the response. |
| 277 | + /// </summary> |
| 278 | + /// <param name="response">Response json retrieved from the database</param> |
| 279 | + /// <param name="context">FindRequestContext for the GET request.</param> |
| 280 | + /// <returns>Additional fields that are present in the response</returns> |
| 281 | + private static HashSet<string> DetermineExtraFieldsInResponse(JsonElement response, FindRequestContext context) |
| 282 | + { |
| 283 | + HashSet<string> fieldsPresentInResponse = new(); |
| 284 | + |
| 285 | + foreach (JsonProperty property in response.EnumerateObject()) |
| 286 | + { |
| 287 | + fieldsPresentInResponse.Add(property.Name); |
| 288 | + } |
| 289 | + |
| 290 | + // context.FieldsToBeReturned will contain the fields requested in the $select clause. |
| 291 | + // If $select clause is absent, it will contain the list of columns that can be returned in the |
| 292 | + // response taking into account the include and exclude fields configured for the entity. |
| 293 | + // So, the other fields in the response apart from the fields in context.FieldsToBeReturned |
| 294 | + // are not required. |
| 295 | + return fieldsPresentInResponse.Except(context.FieldsToBeReturned).ToHashSet(); |
| 296 | + } |
| 297 | + |
| 298 | + /// <summary> |
| 299 | + /// Helper function that removes the extra fields from each item of a list of json elements. |
| 300 | + /// </summary> |
| 301 | + /// <param name="jsonElementList">List of Json Elements with extra fields</param> |
| 302 | + /// <param name="extraFields">Additional fields that needs to be removed from the list of Json elements</param> |
| 303 | + /// <returns>List of Json Elements after removing the additional fields</returns> |
| 304 | + private static List<JsonElement> RemoveExtraFieldsInResponseWithMultipleItems(List<JsonElement> jsonElementList, IEnumerable<string> extraFields) |
| 305 | + { |
| 306 | + for (int i = 0; i < jsonElementList.Count; i++) |
| 307 | + { |
| 308 | + jsonElementList[i] = RemoveExtraFieldsInResponseWithSingleItem(jsonElementList[i], extraFields); |
| 309 | + } |
| 310 | + |
| 311 | + return jsonElementList; |
| 312 | + } |
| 313 | + |
| 314 | + /// <summary> |
| 315 | + /// Helper function that removes the extra fields from a single json element. |
| 316 | + /// </summary> |
| 317 | + /// <param name="jsonElement"> Json Element with extra fields</param> |
| 318 | + /// <param name="extraFields">Additional fields that needs to be removed from the Json element</param> |
| 319 | + /// <returns>Json Element after removing the additional fields</returns> |
| 320 | + private static JsonElement RemoveExtraFieldsInResponseWithSingleItem(JsonElement jsonElement, IEnumerable<string> extraFields) |
| 321 | + { |
| 322 | + JsonObject? jsonObject = JsonObject.Create(jsonElement); |
| 323 | + |
| 324 | + if (jsonObject is null) |
| 325 | + { |
| 326 | + throw new DataApiBuilderException( |
| 327 | + message: "While processing your request the server ran into an unexpected error", |
| 328 | + statusCode: System.Net.HttpStatusCode.InternalServerError, |
| 329 | + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); |
| 330 | + } |
| 331 | + |
| 332 | + foreach (string extraField in extraFields) |
| 333 | + { |
| 334 | + jsonObject.Remove(extraField); |
| 335 | + } |
| 336 | + |
| 337 | + return JsonSerializer.SerializeToElement(jsonObject); |
| 338 | + } |
| 339 | + |
240 | 340 | /// <summary> |
241 | 341 | /// Helper function returns an OkObjectResult with provided arguments in a |
242 | 342 | /// form that complies with vNext Api guidelines. |
|
0 commit comments