-
Notifications
You must be signed in to change notification settings - Fork 331
Expand file tree
/
Copy pathRestRequestContext.cs
More file actions
218 lines (191 loc) · 7.74 KB
/
RestRequestContext.cs
File metadata and controls
218 lines (191 loc) · 7.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Specialized;
using System.Net;
using System.Text.Json;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Parsers;
using Azure.DataApiBuilder.Service.Exceptions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.OData.UriParser;
namespace Azure.DataApiBuilder.Core.Models;
/// <summary>
/// RestRequestContext defining the properties that each REST API request operations have
/// in common.
/// </summary>
public abstract class RestRequestContext
{
protected RestRequestContext(string entityName, DatabaseObject dbo)
{
EntityName = entityName;
DatabaseObject = dbo;
}
/// <summary>
/// The target Entity on which the request needs to be operated upon.
/// </summary>
public string EntityName { get; }
/// <summary>
/// The database object associated with the target entity.
/// </summary>
public DatabaseObject DatabaseObject { get; }
/// <summary>
/// Field names of the entity that are queried in the request.
/// </summary>
public List<string> FieldsToBeReturned { get; set; } = new();
/// <summary>
/// Dictionary of primary key and their values specified in the request.
/// When there are multiple values, that means its a composite primary key.
/// Based on the operation type, this property may or may not be populated.
/// </summary>
public virtual Dictionary<string, object> PrimaryKeyValuePairs { get; set; } = new();
/// <summary>
/// AST that represents the filter part of the query.
/// Based on the operation type, this property may or may not be populated.
/// </summary>
public virtual FilterClause? FilterClauseInUrl { get; set; }
/// <summary>
/// List of OrderBy Columns which represent the OrderByClause from the URL.
/// Based on the operation type, this property may or may not be populated.
/// </summary>
public virtual List<OrderByColumn>? OrderByClauseInUrl { get; set; }
/// <summary>
/// List of OrderBy Columns which represent the OrderByClause using backing columns.
/// Based on the operation type, this property may or may not be populated.
/// </summary>
public virtual List<OrderByColumn>? OrderByClauseOfBackingColumns { get; set; }
/// <summary>
/// Dictionary of field names and their values given in the request body.
/// Based on the operation type, this property may or may not be populated.
/// </summary>
public virtual Dictionary<string, object?> FieldValuePairsInBody { get; set; } = new();
/// <summary>
/// NVC stores the query string parsed into a NameValueCollection.
/// </summary>
public NameValueCollection ParsedQueryString { get; set; } = new();
/// <summary>
/// Raw query string from the HTTP request (URL-encoded).
/// Used to preserve encoding for special characters in query parameters.
/// </summary>
public string RawQueryString { get; set; } = string.Empty;
/// <summary>
/// String holds information needed for pagination.
/// Based on request this property may or may not be populated.
/// </summary>
public string? After { get; set; }
/// <summary>
/// uint holds the number of records to retrieve.
/// Based on request this property may or may not be populated.
/// </summary>
public int? First { get; set; }
/// <summary>
/// Is the result supposed to be multiple values or not.
/// </summary>
public bool IsMany { get; set; }
/// <summary>
/// The database engine operation type this request is.
/// </summary>
public EntityActionOperation OperationType { get; set; }
/// <summary>
/// A collection of all unique column names present in the request.
/// </summary>
public ISet<string> CumulativeColumns { get; } = new HashSet<string>();
/// <summary>
/// Populates the CumulativeColumns property with a unique list
/// of all columns present in a request. Primarily used
/// for authorization purposes.
/// # URL Route Components: PrimaryKey Key/Value Pairs
/// # Query String components: $f (Column filter), $filter (FilterClause /row filter), $orderby clause
/// # Request Body: FieldValuePairs in body
/// </summary>
/// <returns>
/// Returns true on success, false on failure.
/// </returns>
public void CalculateCumulativeColumns(ILogger logger, HttpContext context)
{
try
{
if (PrimaryKeyValuePairs.Count > 0)
{
CumulativeColumns.UnionWith(PrimaryKeyValuePairs.Keys);
}
if (FieldsToBeReturned.Count > 0)
{
CumulativeColumns.UnionWith(FieldsToBeReturned);
}
if (FilterClauseInUrl is not null)
{
ODataASTFieldVisitor visitor = new();
FilterClauseInUrl.Expression.Accept(visitor);
CumulativeColumns.UnionWith(visitor.CumulativeColumns);
}
if (OrderByClauseInUrl is not null)
{
CumulativeColumns.UnionWith(OrderByClauseInUrl.Select(col => col.ColumnName));
}
if (FieldValuePairsInBody.Count > 0)
{
CumulativeColumns.UnionWith(FieldValuePairsInBody.Keys);
}
}
catch (Exception e)
{
// Exception not rethrown as returning false here is gracefully handled by caller,
// which will result in a 403 Unauthorized response to the client.
logger.LogError(
exception: e,
message: "{correlationId} Error in ODATA_AST_COLUMN_VISITOR traversal due to:\n{e.Message}",
HttpContextExtensions.GetLoggerCorrelationId(context),
e.Message);
throw new DataApiBuilderException(
message: "$filter query parameter is not well formed.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest,
innerException: e);
}
}
/// <summary>
/// Modifies the contents of FieldsToBeReturned.
/// This method is only called when FieldsToBeReturned is empty.
/// </summary>
/// <param name="fields">Collection of fields to be returned.</param>
public void UpdateReturnFields(IEnumerable<string> fields)
{
FieldsToBeReturned = fields.ToList();
}
/// <summary>
/// Tries to parse the json request body into FieldValuePairsInBody dictionary
/// </summary>
public void PopulateFieldValuePairsInBody(JsonElement? jsonBody)
{
string? payload = jsonBody.ToString();
if (!string.IsNullOrEmpty(payload))
{
try
{
Dictionary<string, object?>? fieldValuePairs = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
if (fieldValuePairs is not null)
{
FieldValuePairsInBody = fieldValuePairs;
}
else
{
throw new JsonException("Failed to deserialize the request body payload");
}
}
catch (JsonException ex)
{
throw new DataApiBuilderException(
message: "The request body is not in a valid JSON format.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest,
innerException: ex);
}
}
else
{
FieldValuePairsInBody = new();
}
}
}