-
Notifications
You must be signed in to change notification settings - Fork 240
/
Copy pathBaseQueryStructure.cs
208 lines (182 loc) · 8.15 KB
/
BaseQueryStructure.cs
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
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Net;
using Azure.DataApiBuilder.Auth;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Services;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries;
using HotChocolate.Language;
namespace Azure.DataApiBuilder.Core.Resolvers
{
public class BaseQueryStructure
{
/// <summary>
/// The Entity name associated with this query as appears in the config file.
/// </summary>
public string EntityName { get; set; }
/// <summary>
/// The alias of the entity as used in the generated query.
/// </summary>
public virtual string SourceAlias { get; set; }
/// <summary>
/// The metadata provider of the respective database.
/// </summary>
public ISqlMetadataProvider MetadataProvider { get; }
/// <summary>
/// The DatabaseObject associated with the entity, represents the
/// database object to be queried.
/// </summary>
public DatabaseObject DatabaseObject { get; protected set; } = null!;
/// <summary>
/// The columns which the query selects
/// </summary>
public List<LabelledColumn> Columns { get; }
/// <summary>
/// Counter.Next() can be used to get a unique integer within this
/// query, which can be used to create unique aliases, parameters or
/// other identifiers.
/// </summary>
public IncrementingInteger Counter { get; }
/// <summary>
/// Parameters values required to execute the query.
/// </summary>
public Dictionary<string, DbConnectionParam> Parameters { get; set; }
/// <summary>
/// Predicates that should filter the result set of the query.
/// </summary>
public List<Predicate> Predicates { get; }
/// <summary>
/// Used for parsing GraphQL filter arguments.
/// </summary>
public GQLFilterParser GraphQLFilterParser { get; protected set; }
/// <summary>
/// Authorization Resolver used within SqlQueryStructure to get and apply
/// authorization policies to requests.
/// </summary>
public IAuthorizationResolver AuthorizationResolver { get; }
/// <summary>
/// DbPolicyPredicates is a string that represents the filter portion of our query
/// in the WHERE Clause added by virtue of the database policy.
/// </summary>
public Dictionary<EntityActionOperation, string?> DbPolicyPredicatesForOperations { get; set; } = new();
public const string PARAM_NAME_PREFIX = "@";
public BaseQueryStructure(
ISqlMetadataProvider metadataProvider,
IAuthorizationResolver authorizationResolver,
GQLFilterParser gQLFilterParser,
List<Predicate>? predicates = null,
string entityName = "",
IncrementingInteger? counter = null)
{
Columns = new();
Parameters = new();
Predicates = predicates ?? new();
Counter = counter ?? new IncrementingInteger();
MetadataProvider = metadataProvider;
GraphQLFilterParser = gQLFilterParser;
AuthorizationResolver = authorizationResolver;
// Default the alias to the empty string since this base constructor
// is called for requests other than Find operations. We only use
// SourceAlias for Find, so we leave empty here and then populate
// in the Find specific contractor.
SourceAlias = string.Empty;
if (!string.IsNullOrEmpty(entityName))
{
EntityName = entityName;
DatabaseObject = MetadataProvider.EntityToDatabaseObject[entityName];
}
else
{
EntityName = string.Empty;
DatabaseObject = new DatabaseTable(schemaName: string.Empty, tableName: string.Empty);
}
}
/// <summary>
/// Add parameter to Parameters and return the name associated with it
/// </summary>
/// <param name="value">Value to be assigned to parameter, which can be null for nullable columns.</param>
/// <param name="paramName"> The name of the parameter - backing column name for table/views or parameter name for stored procedures.</param>
public virtual string MakeDbConnectionParam(object? value, string? paramName = null, bool lengthOverride = false)
{
string encodedParamName = GetEncodedParamName(Counter.Next());
if (!string.IsNullOrEmpty(paramName))
{
Parameters.Add(encodedParamName,
new(value,
dbType: GetUnderlyingSourceDefinition().GetDbTypeForParam(paramName),
sqlDbType: GetUnderlyingSourceDefinition().GetSqlDbTypeForParam(paramName),
length: lengthOverride ? -1 : GetUnderlyingSourceDefinition().GetLengthForParam(paramName)));
}
else
{
Parameters.Add(encodedParamName, new(value));
}
return encodedParamName;
}
/// <summary>
/// Helper method to create encoded parameter name.
/// </summary>
/// <param name="counterValue">The counter value used as a suffix in the encoded parameter name.</param>
/// <returns>Encoded parameter name.</returns>
public static string GetEncodedParamName(ulong counterValue)
{
return $"{PARAM_NAME_PREFIX}param{counterValue}";
}
/// <summary>
/// Creates a unique table alias.
/// </summary>
public string CreateTableAlias()
{
return $"table{Counter.Next()}";
}
/// <summary>
/// Returns the SourceDefinitionDefinition for the entity(table/view) of this query.
/// </summary>
public SourceDefinition GetUnderlyingSourceDefinition()
{
return MetadataProvider.GetSourceDefinition(EntityName);
}
/// <summary>
/// Extracts the *Connection.items query field from the *Connection query field
/// </summary>
/// <returns> The query field or null if **Conneciton.items is not requested in the query</returns>
internal static FieldNode? ExtractQueryField(FieldNode connectionQueryField)
{
FieldNode? itemsField = null;
FieldNode? groupByField = null;
foreach (ISelectionNode node in connectionQueryField.SelectionSet!.Selections)
{
FieldNode field = (FieldNode)node;
string fieldName = field.Name.Value;
if (fieldName == QueryBuilder.PAGINATION_FIELD_NAME)
{
itemsField = field;
}
else if (fieldName == QueryBuilder.GROUP_BY_FIELD_NAME)
{
groupByField = field;
}
}
if (itemsField != null && groupByField != null)
{
// This is temporary and iteratively we will allow both items and groupby in same query.
throw new DataApiBuilderException(
message: "Cannot have both groupBy and items in the same query",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
return groupByField is null ? itemsField : groupByField;
}
/// <summary>
/// Extracts the *Connection.items schema field from the *Connection schema field
/// </summary>
internal static IObjectField ExtractItemsSchemaField(IObjectField connectionSchemaField)
{
return GraphQLUtils.UnderlyingGraphQLEntityType(connectionSchemaField.Type).Fields[QueryBuilder.PAGINATION_FIELD_NAME];
}
}
}