From 14c832c258d4687ec5e0d8cfe0e21d7fb3bbd4e8 Mon Sep 17 00:00:00 2001 From: Bhaskar Mallapragada Date: Thu, 13 Oct 2022 09:15:32 -0700 Subject: [PATCH 01/31] Adding the ability to filter queries on nested object fields. --- .../CosmosTests/QueryFilterTests.cs | 29 +++++++++++++++ src/Service/Models/GraphQLFilterParsers.cs | 37 ++++++++++++++----- src/Service/Resolvers/CosmosQueryBuilder.cs | 2 +- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/Service.Tests/CosmosTests/QueryFilterTests.cs b/src/Service.Tests/CosmosTests/QueryFilterTests.cs index bbed23722f..70e89a0e2d 100644 --- a/src/Service.Tests/CosmosTests/QueryFilterTests.cs +++ b/src/Service.Tests/CosmosTests/QueryFilterTests.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Service.Resolvers; +using HotChocolate; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -548,6 +549,34 @@ public async Task TestInputObjectWithOnlyNullFieldsEvaluatesToFalse() await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); } + /// + /// Test filters on nested object + /// + [TestMethod] + public async Task TestFilterOnNestedFields() + { + string gqlQuery = @"{ + planets(first: 1, " + QueryBuilder.FILTER_FIELD_NAME + @" : {character : {name : {eq : ""planet character""}}}) + { + items { + id + name + character { + id + type + name + homePlanet + primaryFunction + } + } + } + }"; + + string dbQuery = "SELECT top 1 c.id, c.name, c.character FROM c where c.character.name = \"planet character\""; + //string dbQuery = "select c.name from c where c.character.name = \"planet character\""; + await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); + } + [ClassCleanup] public static void TestFixtureTearDown() { diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index c4d2d0de51..4b95e9cd16 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; @@ -54,6 +55,8 @@ public static Predicate Parse( bool fieldIsAnd = string.Equals(name, $"{PredicateOperation.AND}", StringComparison.OrdinalIgnoreCase); bool fieldIsOr = string.Equals(name, $"{PredicateOperation.OR}", StringComparison.OrdinalIgnoreCase); + InputObjectType filterInputObjectType = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentObject.Fields[name]); + if (fieldIsAnd || fieldIsOr) { PredicateOperation op = fieldIsAnd ? PredicateOperation.AND : PredicateOperation.OR; @@ -74,21 +77,37 @@ public static Predicate Parse( else { List subfields = (List)fieldValue; - predicates.Push(new PredicateOperand(ParseScalarType( - ctx, - argumentSchema: filterArgumentObject.Fields[name], - name, - subfields, - schemaName, - sourceName, - sourceAlias, - processLiterals))); + + if (!IsSingularType(filterInputObjectType.Name)) + { + return Parse(ctx, + filterArgumentObject.Fields[name], + subfields, + schemaName, sourceName + "." + name, sourceAlias + "." + name, table, processLiterals); + } + else + { + predicates.Push(new PredicateOperand(ParseScalarType( + ctx, + argumentSchema: filterArgumentObject.Fields[name], + name, + subfields, + schemaName, + sourceName, + sourceAlias, + processLiterals))); + } } } return MakeChainPredicate(predicates, PredicateOperation.AND); } + static bool IsSingularType(string name) + { + return new string[] { "StringFilterInput", "IntFilterInput", "BoolFilterInput", "IdFilterInput" }.Contains(name); + } + /// /// Calls the appropriate scalar type filter parser based on the type of /// the fields diff --git a/src/Service/Resolvers/CosmosQueryBuilder.cs b/src/Service/Resolvers/CosmosQueryBuilder.cs index f587ba16c9..f0395cb7d5 100644 --- a/src/Service/Resolvers/CosmosQueryBuilder.cs +++ b/src/Service/Resolvers/CosmosQueryBuilder.cs @@ -35,7 +35,7 @@ public string Build(CosmosQueryStructure structure) protected override string Build(Column column) { - return _containerAlias + "." + column.ColumnName; + return column.TableAlias + "." + column.ColumnName; } protected override string Build(KeysetPaginationPredicate? predicate) From 6c80be87858ee796151865b420adc1e89bea504e Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 27 Oct 2022 14:22:18 -0700 Subject: [PATCH 02/31] Fix formatting --- src/Service.Tests/CosmosTests/QueryFilterTests.cs | 3 +-- src/Service/Models/GraphQLFilterParsers.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Service.Tests/CosmosTests/QueryFilterTests.cs b/src/Service.Tests/CosmosTests/QueryFilterTests.cs index 70e89a0e2d..e126239d98 100644 --- a/src/Service.Tests/CosmosTests/QueryFilterTests.cs +++ b/src/Service.Tests/CosmosTests/QueryFilterTests.cs @@ -2,7 +2,6 @@ using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Service.Resolvers; -using HotChocolate; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -572,7 +571,7 @@ public async Task TestFilterOnNestedFields() } }"; - string dbQuery = "SELECT top 1 c.id, c.name, c.character FROM c where c.character.name = \"planet character\""; + string dbQuery = "SELECT top 1 c.id, c.name, c.character FROM c where c.character.name = \"planet character\""; //string dbQuery = "select c.name from c where c.character.name = \"planet character\""; await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); } diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index 4b95e9cd16..8cf9d23bd2 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -103,7 +103,7 @@ public static Predicate Parse( return MakeChainPredicate(predicates, PredicateOperation.AND); } - static bool IsSingularType(string name) + static bool IsSingularType(string name) { return new string[] { "StringFilterInput", "IntFilterInput", "BoolFilterInput", "IdFilterInput" }.Contains(name); } From 97c14a72db3f7981db68b2685a22bdce01d7f6ba Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 27 Oct 2022 15:37:46 -0700 Subject: [PATCH 03/31] Fix merge conflict --- src/Service/Models/GraphQLFilterParsers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index 8cf9d23bd2..bf2c0fae21 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -83,7 +83,7 @@ public static Predicate Parse( return Parse(ctx, filterArgumentObject.Fields[name], subfields, - schemaName, sourceName + "." + name, sourceAlias + "." + name, table, processLiterals); + schemaName, sourceName + "." + name, sourceAlias + "." + name, sourceDefinition, processLiterals); } else { From c8360f0ea34220eb88b64ca75075628b52aef9e0 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 8 Nov 2022 00:12:19 -0800 Subject: [PATCH 04/31] Nested Filter design --- docs/internals/NestedFilteringForSQL.md | 156 ++++++++++++++++++ .../nested-filter-exists-subquery-plan.png | Bin 0 -> 25135 bytes .../nested-filter-inner-join-plan.png | Bin 0 -> 21948 bytes src/Cli/src/Properties/launchSettings.json | 8 + 4 files changed, 164 insertions(+) create mode 100644 docs/internals/NestedFilteringForSQL.md create mode 100644 docs/internals/nested-filter-exists-subquery-plan.png create mode 100644 docs/internals/nested-filter-inner-join-plan.png create mode 100644 src/Cli/src/Properties/launchSettings.json diff --git a/docs/internals/NestedFilteringForSQL.md b/docs/internals/NestedFilteringForSQL.md new file mode 100644 index 0000000000..dac1f04e96 --- /dev/null +++ b/docs/internals/NestedFilteringForSQL.md @@ -0,0 +1,156 @@ +# Nested Filtering + +## Scope +This doc describes the design and high level implementation details of nested filtering for SQL databases. + +## What does nested filtering mean? +When two entities are related to each other, the capability to filter rows of one entity based on the values of the related entity is termed as nested filtering. + +For an example configuration: +```json + "Comic": { + "source": "comics", + ... + "relationships": { + "myseries": { + "cardinality": "one", + "target.entity": "series" + } + } + }, + "series": { + "source": "series", + ... + "relationships": { + "comics": { + "cardinality": "many", + "target.entity": "Comic" + } + } +``` + +the following is a nested filter query that tries to obtain those comics that have the name of their `series` set to `Foundation`: +```graphql +{ + comics (filter: { myseries: { name: { eq: "Foundation" }}} ){ + items { + id + title + } + } +} +``` + +## Why do we need nested filtering? +A GraphQL client application needs to retrieve more data and then apply a filter in the client side in order to get the same results as the nested filtering query returns. +For the above example, without nested filtering, the client has to query all the comics with myseries first, only to discard the rows where myseries.name = `Foundation` +```graphql +{ + comics { + items { + id + title + myseries { + name + } + } + } +} +``` + +Nested Filtering is useful to minimize the amount of data that is transferred as response from the backend. + +## How to achieve nested filtering? +1. Have a GraphQL Schema that allows filtering arguments which represent filter inputs of related entities. + We already fulfill this requirement by providing such arguments for each of the related entities. + For the above example, we already create the `ComicFilterInput` like: + + ```graphql + """ + Filter input for Comic GraphQL type + """ + input ComicFilterInput { + id: IntFilterInput + title: StringFilterInput + volume: IntFilterInput + categoryName: StringFilterInput + series_id: IntFilterInput + + """ + Filter options for myseries + """ + myseries: seriesFilterInput + + and: [ComicFilterInput] + or: [ComicFilterInput] + } + ``` + +2. Generate the correct SQL query for the GraphQL nested filter. + - MsSql + For Azure SQL, the corresponding SQL query for the above example is: + + Option 1: + ```sql + SELECT + TOP 100 [table0].[id] AS [id], + [table0].[title] AS [title] + FROM + [dbo].[comics] AS [table0] + WHERE + 1 = 1 + AND EXISTS ( + SELECT 1 + FROM [dbo].[series] AS [table1] + WHERE [table1].[name] = 'Foundation' + AND [table0].[series_id] = [table1].[id] + ) + ORDER BY + [table0].[id] ASC FOR JSON PATH, + INCLUDE_NULL_VALUES + ``` + + Notice, it has the `EXISTS` clause with a subquery in it(that represents the related entity whose values are to be used to do the filtering) + where we do a join with the outer query(that represents the parent entity whose rows are to be filtered) so that the filtering happens only on the related rows. For example, above, we add an `EXISTS` clause for the `series` subquery joined with the `comics` table from the outer query. + All additional predicates related to `series` are moved into the subquery. + + The plan seen for this query with `EXISTS` clause looks like: + ![Exists_subquery_plan](./nested-filter-exists-subquery-plan.png) + + Option 2: + ```sql + SELECT + TOP 100 [table0].[id] AS [id], + [table0].[title] AS [title] + FROM + [dbo].[comics] AS [table0] + INNER JOIN + [dbo].[series] AS [table1] ON + [table0].[series_id] = [table1].[id] + AND [table1].[name] = 'Foundation' + WHERE + 1 = 1 + ORDER BY + [table0].[id] ASC FOR JSON PATH, + INCLUDE_NULL_VALUES + ``` + + In this query, we perform an INNER JOIN between the parent entity and each of the entities representing the nested filter with any additional predicates applied to the join itself. For example, `series` is inner joined with `comics` and the predicate on `series` is added to the join clause. + + + The plan seen for this inner join query looks like: + ![Inner_join_query_plan](./nested-filter-inner-join-plan.png) + +As you can see, there are two scans involved in option 1 with `EXISTS` subquery whereas option 2 with `INNER JOIN` involves one scan and one seek. Since scans are costlier than seeks, we choose option 2 with inner joins for generating the equivalent SQL query for such a GraphQL request involing nested filters. + +## Implementation Details + +- When we parse the GraphQL filter arguments, we can identify if it is a nested filter object when the type of filter input is not a scalar i.e. NOT any of String, Boolean, Integer or Id filter input. +- Once the nested filtering scenario is identified, we need to identify if it is a relational database(SQL) scenario or non-relational. If the source definition of the entity that is being filtered has non-zero primary key count, it is a SQL scenario. +- Create a `SqlJoinStructure` for the nested filter object e.g. `series` so as to join with the parent entity - `comics`. The join predicate will be equality of primary keys of the nested and parent entities. +- Add this `SqlJoinStructure` to the `Joins` list property of the `SqlQueryStructure` representing the parent (`comics`). +- Recursively parse and obtain the predicates on the nested filter object while passing the original `Joins` property to subsequent recursive calls. +- All additional scalar predicates obtained from recursively parsing the nested filter object are added to the `SqlJoinStructure` corresponding to that nested filter. +- Create `SqlJoinStructure` for each subsequent recursive nested filter with join predicates between the caller and the called entities. For example, if `series` were to be further filtered based on its related `author` entity, we would create the `SqlJoinStructure` for `author` with have join predicates between primary key of `series` and `author`. However, we add this `SqlJoinStructure` to the same `Joins` property of the original parent entity that is passed down. This flattens all the inner joins and all the additional scalar predicates are appropriately added to the correct `SqlJoinStructure`. +- Continue with rest of the filters on the parent entity and eventually return a chain of predicates from the filter parser. If there were only nested filters, we wouldn't see any additional `WHERE` clause predicates - only joins. +- Build the join clause while building the original query structure by an `INNER JOIN`s for each of the `SqlJoinStructure` in the `Joins` list property. diff --git a/docs/internals/nested-filter-exists-subquery-plan.png b/docs/internals/nested-filter-exists-subquery-plan.png new file mode 100644 index 0000000000000000000000000000000000000000..0e989aca1ffc7c2cfa92c50d60c411ff89525666 GIT binary patch literal 25135 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 literal 0 HcmV?d00001 diff --git a/docs/internals/nested-filter-inner-join-plan.png b/docs/internals/nested-filter-inner-join-plan.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8f9a55ac24b9a418ee4025b15dc10a2613cfd3 GIT binary patch literal 21948 zcmeFYXIN8P*Dj2TTiqb&Mk&%1w^%3?mcoKh{J65mX+Q14(LB5Ly9y$@RTf9rpId-uG)W4YZ+Oi1WKosCa$rc#Oa zz*YEkx{Kbw`$eNvB+d&1@^FqN7st-$|NC9@{g*ziuhL3{L%_E;$ny&edk^jcK0ZLZ z4&Uufm+v_Z+z8#Nsos0AHJsv+uzTkgeJ*@y=PDE={GXSPT{hjhEioV7X-240MnB+P zC>hOa9;y3b98x~lY*+hv%PkPR73b3D-RZgz{@jz>4c!kGrh_~bGOv|6mtnf_v<#=q#Xn|mbqu>9z*wBaqHlt z+~IJOH>w=hqH4N0*ux#riT9tILt!usFEonFV)Rs`uUX!)U&nd2*+gK*w*PWbYY4M< zZ*cMd*)rtyiPeR!Vfkvrjk~zd)OLWuROGEzq_ZRN90EK)-#%_Xjd4{dfa>B*v5amn zj??%O_zXRRI*$H>iE>hGZ#m2frKpNLUi{gT*p@Ji9Ii`)CD9h{Pf3^a57B$p?(^)} zuU(^mjwe?y>JK7^8!~+32le}@uiFx<+S~~xjO3A;v@l!^?vLzb59l5tp~favZhq1r zD0v#Angda)u~${eCTkH@vJvsTuV)H)p+gPHb7Qp~Q!weV-Y}{)6$zuOTDU+^pxcjG z-p?6|cBBwWL>5(RLF~)0o^K@VHJ6IeECYw2fDy_6Y;@xNaH3frCr&Ut6T$f>D?ULq+#-*6BP_f0{^Ks){3Jw{R zxaA#vdR*r^+_=5lrAYGR1PJ>j7jjLvbaC};Mb8r^k#q4j*MotNBP{t_b)U;%+(OHj z`6*6RE2@N+u_L-(;nV8lwkPO`;=n53{o{{=9f?~i*gMgv5$qXrK`v4})yT%PRIa9H zBch7iY?p2Z>fV5#!z><RufN;#qwskk%9@K>O*Lk3b6XYHaFj{?yI=7+?hS)#k)_cM*4-P>y!H+z@Xx}t zY>I}q?Nard4y)!Ka$LNa{-(atTaV|w*yQ?jZn&=4&%lyIv13BdhURh@bH{NEIFwtB ztPi7hP0_}4=hws zRnc|j)QyfH2d1jihWDu{MdrOlM+;P1*)pz@Gd0$*Vx8*Jn(M(espiog@r}47?P`op zHdD-g!D}M4nq{IzmmcTW0n1qXvp1;KY{!H|4sx>S9I7MIj#7bMKQXH>$%Y=f-M4xM|pP51vN~ zjC&I;L1^TTS-V*1*4(R&4>foOOH?#(3w5zY0kWNDRZ+H{Y=y-=ZcqWeUQ;0-gz8-^Cg3Jd>-kxHO*2Ecr^=bg{7>M}|?$ zJSS*n?STD(9PHi*&x7WuEGo>qx>p5<4;zb}gNXNInDpS5T5znigYh*ew``*6MZYnV zK39TV^KXDA*-gW$prR)iJ!;}>LQr3O!xE((9QjKvL&wKMbCEo+mrM}r;^(Nr&?G(o zQb&bS7=s0_aGj`2(AFY{vr1iOYHGLL7F#?bVyj$c+!|axY*bDx%FpYkKqcb`#crWp zmLaXi4!HL0)SC|IdcYA71WW^OAE|Z9K@*Owo6Cp@JHK-`>6_ zR^zJyHE0-QN=7UvhkDm#C(?cOdhZZh0LLs4J7HLYZ2 z2VLDjBYsdf{st}aEc8@+zyE2XGHLNE+FLzMiV|=bL z&)cAK+U}U2iA5P%!+GGIiwJeK%4VAL)vcnCx(TVeHYk%Nn4*0rs2 z-OWu3wLN-g^rYItdS8=`b%``Tz22akylxh%FcV`Gt6@M2wfJ~nY%J~&4^&ZC*}3V+ zxu`{Uu8Pu0^ zDoX;#o5j8N7o#$CiBAJxCRJfwFt@O&+vzo}O}7{!K0{^}D0m@*-Fp*sMFx!=^&+m6 zyIQo$A2X9p9jOs-N~obntH)*6v9`d~KNo_S&{I=S^>L6t2ZLj@*`dxj8a`;n7K?7r zuJfbhc#rkoYlGGi=OlTq;p~a~WVck~j9cr5|AgVZjB^^Y!+K*|(i59CtI>`%c%B2d zv7ooHbES~zl;BYZb6^Iez$Dj=dumXw(M|2jPQZ1D)_{vN6me3)r#M9GRvpLE=|c?F zvbym_%KbWXlhU+zj{Q|rehD6_-s25}9=^dNc)P_Y?B@j4@J00U4I0s=0Xe=%zHerd zJ4#GzhFiAm1ip#X&>d$zb~sxX^l7nYs%EvPR?|xUwUgqF$NkAoV>;#|QI!++i{mx{ zXzj`bXFFSLD3=kAs(rP>)Z3^jb#V7@q7HH_n>-qjHz!ap8T*(T4#cO(Fa|TY6yX6) zzK51@TW66Mb3+{qJMHfx6Ye6om4Xn~BKUDf~ zP4RGpsspa3I#k`fC2bNzPH3fCYE)+MrhZN|RC{tJzbZQr!6C5?I_+wTjt#~Rxw*_- zT+&$(w8liM^}rybdY!jXU0Owno94*SBgy4%Iw68;FR}+&n;gWda)RHOo>(b|0FgD` zt2G?0DA_S{9xCeU-$pI_J?d&^FKc?fLk*I0IdO?--U?%KaYqx5Np<9jZ z3{$MHWnl14#f}!Ipq$&>5k|yMCyT1~Aun7ZGuNxj!}oQqe-l}J5dw^&drTvs$nIrt z!n(hs&Y6HO6AoS1bWqggg&?_#w7cpPoQqdooL3g^8%g)T!jimO4zoN_Zdu^j8CP-c z#T4U^eku&L*!?XG5~*#s;q;0>8}vrE6|gIC6#^ zT9!_@G2U?tX!qIo+|3(|?zr=diR8-Opi*Xhslq~ICKq!gxCL7p7G1~k)(&Y-B-bu@ zp_Yk`jgAXpFB$dY#W=mpYXKHb*T%W%$J92pWY?h8JQu;|=BPu9ZX@ zkaph}0E0CJ2I**}9k9vgg`dk&w;nqYu)odoQBu>;Y2($WA%BL);=rSi#f^XRfk7lG z1?;cQ3mX1~7qns_s_q3?rU(`kuzzoU5)i!KSg+gVECJsrrmPsaZ-+xb6T&%4#gI)_ zE7quMH7$H#{^T8LoFN{_x}}p!PBi+}w=Y*WuZLsNI*fPsEbRUiUHiP!t~`Brum*+C zOmr?P09`)N(7ARa=i#KAt}>8ypX<(gcvRxy6OZn!lZEFW{ln!`i+Wys)x`26VQ5Bo73 z2i4g4eD84#t1i3(vb_W;Lu^t71^i!MiG*lZT*nrxCzpWWA#BUn6%7h8iD$)K2Ta(v z&`VnceA&O@(G)Q>GPr&!$78!6MH?-%?0JzMGkd)?D266FIpM6aL_wtLBjlxDB%?x1)J#}Gk)8ttfm2@Pd3gm zbI~*0`jUB)v=J>>rdSi*ql;em0~kcl*2KYQV^mx&>cOml1BO^3w&y_pySner%H|T9 z1d+x(tSx`=s7!K0EFpNb(BxMlZJap2tNV~iH1CTw?V#Xi{Af-6YR#Vo#ypyB#m?rV zX*aap--Kf=bgtgP7FN<*Vf5BfC(K&5>MtOf4++IQ-E;Ejhm?F1ZV+b-VTkZ(2?ghP zO3yI&=$3&JnPnSv1l-sNUUm3+%s4u-TfV_Mo+3zeN%DO1V0IaSQFDiO-zFK}WOrb> z9kgWF&*V`yY=vEJoA6zNg#4nAz~5RxeM1!zBt7=Vj;>`Krooe3!5SF z?Di8^$oZ_!6?7!sw)5;X-vra<{Ac7kU?qG_)8=IU9WR-Uz#HZIO0fFRr~;6l29FNs zHJ}i)XmS|tQQB@`pIkhdu655}CAs^DE&RfT_u+K^#?BrLzea?>6IptX;F-|P z@2Mca^;OYN&RDtjZB3JPOlpX4YQqm-eD>h;Ew@cPzCu&LmFSTh#uZYw=Qln{*uItWh&jQ_g0I<*g0??`-Xa8B z2;1<4IayTv`A3ft+^-9xxtNWz5T1o%Vzd1DfzEj*Ef>C!zvTY}Ss0Gd6VZ=50KmD& z1HM#uc;YkOf?0x8Gix$7I2s+Akj`)sCjfeMpLPSmI;9qT}WnroW93ys|W7X~n4!QvK}`GVll7NQ4vXf4h2 zb2yw!CS+`X$?)mUrSO)c{c#8$CqaL^#NvY&TSMD7C~LVFgm|Ix%yF8$2`dBHdfI_KZySGI^W&SqC?uDY(NweEq>l74)8U{ZdD|{J1rv_4L*XMc6uhRA9*e9MZ8Vv)*Lbf6{bBbH`!tOsJ+IHk9@3 zV~-WNzFqmZhJ+-TZ1!O((YrbE_xRZxg6+O_8m(c-e;w4daVxX{fxs90Z?54${`Q^L zTiRqu5r6B05$%)hbY@sABmF;pY$pY`#uuLu5_(RW;M^Kfc-)-GWqp@;Hnii_H{S0N ze+x>O6`^S&wyZdxP8(-hr>=f`EbfQ@5eGPh{2A^H+s{5B-PaE7-J7mzki?{_Km_J- zKU?t?w*1vx0#-n4@}iAy?>KR6>p~073C^)uC{zQ%8u%k^sieeJy!cGc^1H3}H$E}8 zu&tgAxM7q|hc5gsekjWEjp~jO-+v7Gz2I{9rVPPL?&96!@dg3HvL|mk|8Z#VL1ODg zwqkywexlDj&8u&1|FJ9e$F$t)F9b+~E*;)M+HX8cs(t4-eAhwvAQ5|!jZX+QE;OG_ z3ic@Sb!W98gNVU(-sPmIb z{{!vc@!gq4_2usUYiT!J5Ib!Ny$?9>pYQA>ej|R;M!+(Ez0oCFdgC{aequ6rQt z_WjjBsvC6tb|4uY+5=$qLP9torrEiUN#hR`&V=Z<>&KsT&Y8yt_ zPpg(ZjCDMYG}d1g@)5Yf(j_JRMkiP20^85vhCG8w-%v(x_b1fd-`M=+-OYq=JM(q@ zgS?cii&F6!Wj$ZL?$su}Q_Bg^k)Jkk+6X=|_{_Q$KjNB~xOweZhv2RTQE;q3J(y(> za&_W^wib-yaO1SC6i|U`G_H#s{!bqv_g!jT%R%cQ*arHxT%D8Lzk69?iGF; zxyf&e{4vXM;!5meSF}|{6p12EvZHpN6Ng^N8nU*2+oz=Y^OyJ5_SN_frP6blNGD{z zq@OEex)5HK@y%2%Dr{L4ne?J{&1b?Xi6eY1hRQ0ZY7>l2wPr&sRQA?wM(q9z?fNOTh@@s z2zGh@%V;ImHpgi5SGRaYuOi$~-Y8CsJikFB(0=q~{mu{YhU?ERoru(gsfs^)b#5LE zd5X8HxbgWLe?p2(i(HFT&^Em56tqu933ayw@3N$+& zzL|RHVR0!cGhZvSe3|}_ycoqKzkE)1bEzsa!K+&HWh{F(Qs6_yZT+z72w~512|qUS zRnIpXP)MDW`1D%Z@L-z1x=}Xd!pWx1h!KJQP|x#O@0LsB_L<{OeHT+L4MpaApl%0_ zWhlz_{95EN3sPaWtyLV60=v z-i`a4rS^f#FSoMonL zqeqp|14=R1dR54Xn1CmsH6t@D-9>)^QGmP9fHXex@cpQLgv8mD3%v%W%_mD4G5M=U z@Gi9*9o(B?^;*T>S3|}{Cqk~#y^?UnCYeFG-Om@46!m3EcX$nR9%5f_59X+qKY28b z23c!0WNptqNvlYd=p~pICF%FR#&;c=k_mHReLH0O*t;@>`pQQp#XBB_Cv}n3tj}`@ zLt^#*YihXVuE5%;jQaQ54hNpk%RCT|HZo@fpGgvFMVOKDIs44GZqcT0rY#gpz;-AM z;_;xD1lJwW1aVVpC94m%;2)2RQrhx70z^p zxNmQ8VtSP=CCC7G++JdQ&Pjo&((wAFvSh!#2c_J8g6%do6kk!V(va*yyv{7>K*AYF zmHO-nhp#4GZn23AJi1%PJvaT(`B0{@`SeOzKc`NsxF=wW*%F-&#&g{H^O@yxJCr}u|?=Jj=O1BeaG<1u@uH1Znl8ba>5l@o7CD;AZd;eOoQT*u148s>{sd!-HN3)i?DSQUS2EKM0{$ggM zT(>M7wq8g=iCRLoze^GJ8UK11Qn^Oi9;CeOP8P2SZs^ic=~%3;8Qnoz68%EW%%`$X z_9zvW8;2Znyy^ok(jQZZJ5ZY$Gf(L{Yoznb)N>*S`?!;kxgVFETB-X62+#p!p#yubk1#}V+`CF&VnNVs22rX!{)zxrP+UATlecYe!n7ypWClv(4WBXRH0c%z zcGah1eevpaADQBd&W8UOmoI6T_pGm><*oLq_-=vkdmawL_hpfG_$s{G!jjpcA_LUx zp75A%*_2al^aiZ)G>prjSwc9sB!v@p;#k>**!4 z*1FRUs(n4{`{CdA!H)BO*z$cG6?Sz?*URAqA51p)k&tRrWtdpt$`Lyk9>&8B;APT~xL3J{at!?qc~kn^=*vX;)_q3j#3JiRPT8)6P+w&RIU~^ zAFt_caV{Uf)$}xsf(}=rL=_n!R(^S>SzN9y-~X0@z%?Ku?5mi#j{O6J<)zfMevQGN z6=stbfhuf%Jk<7OPg_8?BK(ZFjt<_ARr~c;jW~B9Wog4vah?_Z=<(qJ71Wf!az;gR zit(qGJd?22$_8!gqvF$P4?JBU7)!Izxn9?Gi9oMKxA;P&>^(80aZ7z}o4qgHeJXIH z!PSXm40z~&wZ)HlL=0-H7@IjN(JgWkM_?z+PN#ZpV>Z8!^))^LJ!Q8l9-(k<=}6Hn zNPI0K=T?g!wV%;%akBTr~J!~IN@M5l?7aFbyTe8LDff=)Tp|%Q-S+( zgAxO*YhKRk!YT{4UZo}uJ#xWq`#P9Jt!_`;M;z-tk$$;zxT1Y2fNto6NG-81+fM0r zCF{U#LfUNV#Otag2EdP2y{nErU_5yr<{vA0H#8Nwj6XKG8<&VMJ=R;*A{Yr{x;e(Eru zI=ep6=lkUS;gw(GMnnTdkDlq@{ddt)(Fdhpch1}iXR04TmIR~C(r z7brbe-Tg3D*}2&6C=R()pWheR`K3L`-%EQyVNl?CyaqJd-k;=ZW9Y!>zFN=;hhErT z%*UTC>TDe_1kYPSt7Y!h1)l3!2S=P1b4)kgQYE75C|M+pRMCzJ zQW?{ER>`h43h!#5gRho(xKq0;2Q9*0nIs~a$t#yM_6Z5qgcC$<{nk?+jATxzREUfZaGwY#@u<+%d+9~3aZ7~Bta@KCyHOCr909W z*WXZR_!G|g5vdj<<Rs!HrL?P;0T!|(>nHS0K2 zbU)IAo~{}F&(UQQm?K>Mg6;xwDSYzG#N$<$_v#{-K;QYQ0=q!JS}4CpApLxwZNrny zA%(h5XO-Rc)I&5kF54qFB5!Y7hp;=&TXNSrFAe_g34p(Rs?RqXZ-2BFe5QV!mQ|7Z zNTtXqf_hSSEzdI%C_>Z%aPmagg*!*GM7ybrJ{Zv#{%oqLH15t7{&f47<0+mmd)5|l z!u=ty5~_to`mJ71v|Et#AE)*Scisx!45p^SPsbx&1rK1YOGAW*DSAO(PZl=%>htsV zEeH8rFEYw1$Gv`S*%%$t?*9ud%JQ)new(sDu6x$Mz819W=XRMjY5m{=3#6hM&7Nsm z{_uAH59@(4j;H$&%B`1xd8#Dalp6F$jBro?!nT?jtE1HL3~Bh=(0b5Z@!$of(zgUO zv%~AjV<~d8;KNUJVqGc3L_PKa5(+y;>M0r+o@9UbM%yf~Z|PiZm;bi>B!axvo3$Nn z;h+2YR)MiSclvOUX)4&y7K-%pE|8(a1Xp=Z+aBp9O|Sa^(X?!BZT)+Hw@zDxy@-T_ zeUH`jfRAh5tFwN8+DinU7!A9inN@_hvt6vE%;4RpL+>ZzwCO5%MmCPV0BVn+BJYL;DN)M7p3EurA3U}4e&$qtMM%fH^7aXXx2|A~ zY=(A5v?_=kY%O7LzqJ~mNa>rnr#4Y%#o)d{hU)oFW4?E)3lstn+`N8oLKzR{Tf}XH z&ETjlI#c!}_UD#cMkXyhhTFn;gseT|r4EOv9xT^^3@?f(AVP`|uWDN*?EN7TBe{#n z&hWZ+zctuZ7qlQdSVBn1JQOS}0v2=^kT8kys|O}uD&gj5UMkQpbm}4>+sIdoTN~f| zb?1hnTCstQ%s&-kRM4S-F~W>a%S79E&pWH-q%~4@W>dsD`D0O&cN?NUmlh|FVxqcS zJS*T^k5tHuE)X3u@(VxID|0&gpnaO|9w$%=RAtDWAJWm6&w(1=bTc49!Cj63u!q*^ z&hk|0OUm=ms}1w{EMC?(^?WEVN9LbEwrrSR2YRtpgFG(P>J^^aP4zPl7&5RAF0bfINAzS>RU z@_SR7LuQY*22_0-df)c62xGX)HD}@>VUXj+^$zST8XVWKGIy?nkJq+-IrcT;Ew7vu zWsv<0mEIXno4LVqE2SJ(O+hbPE6Q&!4eXcXPiF9(IZOT~U{%t)WuOA}T>^KQJ%02$ zdQaWTz~?A5&CAN_Ap4CHfN)PsXt{YFcAMOU^w(U@d#a_EKl&B(;bHb|`vc}?mxA+P zEm%?Ut-a9^uX?W_PV|GpZVPEA@QE*7k{*Qtp;d_aqa>AplVa|j7ldBy6J6-P<94`2 zR_7vd{3yN{oE}u-|E0wCRDff=A4FW1Vfd#PAXp*XsepHnbj*}T9|%E0uX(5iHxv{} zuX)DAD`M{NhlD-}ivJR*Hx~K8Suop0YkKp@GdWjXnQ+rwrt4G7Y&+1v9nCeRmq(jP zj}II!A)Dyl?!0sdfSKg4lhfxLYA?dSzg9KSP%+TWR#slYBey;STGB-Iqt#ahbOGGaSuXuc(t7A-_tw{ki|!5x(8{eO-Onh*R9pw$mN`?TI-zi1@{ zK!_ooUnUG9;3q8`6C6DZyc=&OKI}P@eAW5*-s2`@G%PF;tdO9#7xZZv5f>6l#wOo%*n&$yb=+CB0DV?%$l6DbdUJ$Fb$E z2QIpux-*GU)+gG*=V+L!wq`8YV2|88m9mrZ-(^y>Xs}(~=z1dD4y_PbU-vzF_;@x& zb6@h*or{MjCBAwGv|zLAxYbbkh7PQvnW6#v`rJx@Rd;0j8vDM=cABuzgTbK9lG)9s zZJ9{M!U&b*c6!<9#b>`Ye?n1V!5POP?4;s#>iV+Z=#Nz%Nh&j@+`Tp`K`^3KXoRi5 zE!l0luk)GGZkfZi{fnvoKaS$WO&miIE%qBA&EoWy`SoRVj?*HZdk8dt23<8q>`TDm z%IDLgJ?1kR(G^|&%VA-R1yM2sSRm|3=dNPH`_p_D93zpFnCa@c0K&4uzIuo=1l=#Q1LVh|$ z`W^>`(<{8|K;i@jXh|aQ{(W_$=CN9YQlmqLxS9E<5iQvdgJ#0%UQZUEj#W4O3|4^r z6wVYgk%Z*9uDdU$As9tKiTCW`x^xGwcsxLrKTf}wleS31s*R2ckG6U7f>}W?3SfZ zd28I$U=wW9lyA$XXFg5$>L2i`?c}x$bXacsX7cNymB*^mF_o|Aq1(egFnlZL;s6Io z!#K@LC7y{tgYZh9@OaqnSDDD)bt$`9fQpxb?_E{DPb4IF}&mx+N=p$o9%NF((#GfI6v6y9T^)|8fC@SAe`ZmgTj~q<3xJs(~%=-JEvs^G_RH0gw z)}Iu65TY33W7Hs+PA0fZL{t_0kaB^0$FEtcr&KZQy-B-{C6a+Wi+6 zGWI!hC{;u2?(6;%(IRUH4Ybl_U<>OOaK<_+r zQehrpQ#@qMj$m1xn`s6cv*`}2OEsiOW$PA*bEZ-7ii5{}epTDhx<+qC%Y;5D*3p?I ziuvBNOQQu*vqMyR-za=gCX*ED*Uo?Q%+pYtNDu9@Eo+;XeJ8TIJAEp3&p8`It=z7o z26(qu@4izFcKR1p4*2rqlWbjTMrvbp?dEWEeE8z+NH8Kv>}&RDrcrFpZ1MKxr#o+| zz9>0y5*1yWr5(fH=zF=W{~W(@c-Q-=iElfo_u9=>rI-7!L|u^bx0SceN>1?{i0>VZ z?079f^J%Z)DZQLdR>UEo(gdJ7#(q8vgy}*;siy+IeEAe1y1>eHfq63)WaqyR_S;1@ zu5W+HiAkTG%rx3s-^a$c02OrrV4oN|_Dl-V3@MhwYrG;O!chy?js=TfRwth+cm2)E z>UM<<|Jr^U*(hJBSCFnSJrRpz=pPDSPn_2T$jH|5y^6--JD+LwQwq*KIna5?T zk*Z+6vw_NaC-s$EI*Y&UOt#q^zaW&i z{Doe#FDID&R*Kx%^Xj7WjFd%(@mxz`*;mU{0T@_^kSLtUF>|5g$Tq(IgJe{oVpcv$+~koZjG;` z8EH7Jn?2e`c>E|9MYCEI)ig;E_~?6leGUNn^iG<75~3oD5ih7}>to$yOYf!xo#Dy_ zVZB|mM9=DEZQDA-x<8f5C2b|wo|?Ag@f}&4@xXby5SR+fs#{v&w%@f+M_kN&9lMvh zi$Y3)LaLTsg3Hqr6#&}px*n^16+c^@_PV<3Q*H%IL^COp|H-tfNHu@2xRl1mU+5H+ z|LDe7`<{X4O2Zcu%Y)y~F6GmPMD7Lo5&r9%;Rx#;c-!~1KLX?_5ffCue!t37jq0(; zZbcF)!GMWd2S*Jza;4Hpk&ByG=gK6r7|>pE>+-oC5lu7$G9NQ<{9Uah^|qfS8Y4?p zE+6%7`Eu%c7&%utCBu~5rORwTIj1%?-2-k;&9G+Fe%o|MVkMT6t-TJwKjuV&m|Sq1 zE4%?<`!M}oE_Wxq8#B#5@oCF+B4a2W027&_Prvx&0YIX$ec}f*mSJ;pho7Racm!l9 zRa~iYWGE@+(!7tpmP5)_Oqd}h@SMEHkHZB@Le721o}E7W6?*69U86^mMOGCg`(A?2 zJyY}HH$3q}3$|~wX!TJ!t20eQD=(HZ_eaZr@pon3h9pSf10oub`B#M+i?s8M!lQqk z)cnsF^?v-8g#YB^I5s7R@M%=PUr(I9DTA+ zKk{jn*M~crSw>SbC0)}>Q;+I?d>y;Wow?S8^SobLhO@p&asn+BuZ7QDIVK zywzu0UQrx(Zij~8Lh44@R@n0TL`eQX6c`53Ywt*;h<^M{bKTJmj<{gf&$hU zpdePC_Zc3G$pcO+z5~3IP{=J1X46aOZ@mO&4P+1ogejt?zx#CRTD(nTgu~U#KRX>wu_eh?c0n1waMOT z|IX_Dto_^C6`qt7w=iHzH+N|6u-BWf(7z9*-cd7L`*M<29KU>az68i+pZQDjX$XEG z$6v67IBH4vDry9dt!)&uJ5P53TwQB8T?&SE&KYgMgsbwt6&KN_f*S^NfOJ+Epxrj^ zGCO}Bd0=h<7;G-DbM><_Oo2M%O4YUdv!=5$`{0NLMMUgSeZV6 zUM)Y-{p6d6=-DvdLHvdNb7iMjfYJ_1^^^JLJ<7i2y_OY|an<&#yfj874Zp ze4gz>e$ozGDsC{?BdZ?6S*!){{X4G|pK zPZdhuc1~;OsV;y5`K8!h`f|^H# zN)$P-6>nhK@z~rhBfUpStAAy|*TebNl3LHcL4^0643H)z&jL&a68^%O&4=O%$H~gD zq@9YV6>zxNlERy&sNDq$a61}pE|LF_34hL{7(?&CsKjqwf5+$03F-0%$OoRHE@G=M zUefD3)VT>5Bq{DFg-59)%+ z+uI4;z=c(_Jr7#eznsi?3&^D+@A0x%-aYzCw0=A*Bu8d*iipv1k7u-)1*#{h!vXe& z?GAaSJz?v99HDULr3bs6c(m*+Z*?n`R$;d&$QDR2q)A&aVdkDy2;D6DzLlgQ zfD4vUE?xg0p-MlJ>&`rR(OJx*KIO!K#=yl#sTyYIe{EAbU&0nZ*aFy`ZLzH)hp|qI zTh*NRj(yoH;vg&rY(d~K>SRg(`#Zwo+_OY7j}ttK=lx`tC+b3S`E<=(B=SGZMDH$V z#hoGVsDQ|mXP@%;%f-ZUNIN~etaT>N6m19t&d7j>K!b<0gdeCWA@`p=^N#&Fn2-$n zK1g#nxogk*-7R3kgm=2U(jW4X9PM7HutV=fP%i+I|83bcGVLZ`0p_~;cSq+?BEHgq zd7J(pzHJb_1ATvwnEDOqK4|&%zn2LdOIEmOpRil!@xS}nM-jm0j{WbW5dVJ%@E&PB z(*=spakW}l#%LFDztx9qefq@cSpyS%&^~kdoNsq|4GNDF+KsHIZygbPZRn^Ot|Iwt ziW~-zJ473+Vphbm~cB;;A!BOu2#hTOOxqJU+&Gq#HyT+!{- z8p(L*PaqkDUF%3i(r6$*oL}meZ|a@#X@k`K@q0hIyP1N`hEIIN_0#HZHm8mMLSXaK z4Z%mmPNa{IoKDTpBqx&4+cA*;2mD!x&9RdpBKl;NGM~ctC=Kn5{buHe`E%`dT!H-cDGVK2b4Pi zny-)}siztHLNDq^w||gKl!QdbXlMpv5H;l0bsFR&6Yo+&AF(`Zv=x$>8Kuks?Y6a7 z>?}iB`&4($O(-4D+E-%ZHd*|6CC}CZwPv{I{O)VXke_4F@1MbIm+XWyj=99n*^U2Y zhArhy`-1Bdr4?~|LRw%Z`iQ99rZRKIH%q_b*FIK)(urFO3xvqv8&c|``y2nAxc5YV z2J*){{ax>idzV97%X??+6i}M9Z*vBtO)9$|>%Q%(3mY_^a$Rszm&EdH%si9v2XA4Eq%e}zqMU6#Ni>Wb$-82U3++ZbnLS@h^bM=^daJK{sgj1R|-IH z8^r*--)LnpiaG3OWi244p6!}L8@^-m-AzSHr}OvLN9B?YV=Rx^k_iu69T758rb&pZ zz11u;N64wUrMorhs(I1t>CGMc9g^m-7gA=&>sYqBkAp^5ENQc}tpwIOK5{o?BzeNP z`Ef#~d}~cszGR}lELZknm95yH(vq&s+=ob2p*&wK(>O{(w`aBEguF^R-g{+oM&Cns z40$jbds6&Y&IfXtEJHgoVfofEcr%9qnRs1Ia_?I+Hz- zi+xJuI^=Zm1!-b;-)Z%Vb%tt_S4v152w$-t9Xa)EpqJ`%j1{F}1f5Nagy_9(iIX#i zpB*f>WT;{j77b4}70u@OibpBxxJj7$sx17CyP0sJ`1&Mw((D&2!AHX}Cp^AHAtp@} zv)@B~u}lH)VdEdosN9I%TmSmT11-GJ*KwwFOT%kv_5an*m4-EWW#NES?EnJPkwrwS zR6q!XO+c2YWwR`T3wwaD21-DI07eK34A6oFDNDdKfU?TICG5~jk=1}8Tf!QJlqD<) ziy&&NI*aogb4wd6MV5-*?Zs=ey@U=braWMv8l(md*+>gQtW8g_+G8K@XPt zey!`{wfB%n81Usa#gK=LzU0;U@J>$L1LlpNc>IN)6_oMNA3!~mp3oP;mEZjdLbit#`(pMAz@#NK8|ba zgQs`p7Q;kNiSt!Ii~EZ4eA+O0D;F{5S8P60!lf>sGFy8ATl;~X;WB1j>5;aJFvKM} z&-f{j!>!E1`*T!1I#P;SL4L(s_Jq{=(-Z{F6Jb zCx&#qCrF~Okj0?aq&~-9cKE!xwWA_UYrtj)Ux9Hcx#gc%(&Ue7raLxrdhtZeXl0vE z5$k0-weNOCM3g0IhUB6B1t7aYkavLh7&I2ROeq63O%_%U1C9I<7TmKssY!5$eid5* z!Wo5VU1d}F9{TBPe%*2p(XF;BCEyZL3=&s=j@=tKLi((7`Z(m}6;6aH93k?3c?ndt zLAE^-KBTL%Hl8k58%^{rM7UfoY$Prog!LEYCA%mt=&_J4rBI4-X+*@sGZ)d>c?y1h zTXW;zNwDV2;gj2|PKls|uw8;4(X|z1*i`w8AtEtw-)Im<23+g~?lK*0 zFRDKrKWflWse!9XT{y{?zRhY56F1lEL#?i&^==D%MWx*t9Wau!tqus;x5$B)<-ZHY z53LeM_|jtwM6BQFSg|&sz6J!8vbVVy?Xxff^`C@(2r8mK>=SG2h^z zZYdX!ibqXRSkPIjxYyR>=3Cdk_|mwT87j;hxLVo z{u-k1!hZA6ggUJ>>%wTBh>}0OP-bzDO>}#HY`V6x`}nd%|EZVC%|ke5e@!>KIqeLE z-x8N(nDIi~AyhkmW1DPzMbQC$Nz9_mlQW_|^I+TU^7$wAFlA5f;Jjoja}+wOi_!B^ z)!d#-9SM#!)Ezy;j{s_^SymN^HhDD@>v;BjxLPm$v{w9KQhT$8enT4ZP&$khQ(`C3 z)IoD(yz4P{u85~2Y%52CrbaosQ;KUDmYFkl5={cGd=O;FM~PFdK!x@`b})Z;r5>kE zJ_t6I%I(SFXu)koO8VhcWp-Jj6S;`yc;)=P)DgS^ml?M$N^!y2ONl*Eq?H_PAQw%+ z^a;Y1%v1V~kD)$qKQ$rcywRT3d9PBC*Ks1T4QuMhWimbR2Jv_8#LAXN;=Ov}6B$2I z9@u(_SM~8{CLV7P|7j39&N;XtdikLoQovfexBo||=&6o~zH>v3`r#WEW5JRgsf24| z)dWXIgq@mE(3ms{t9i1LitnAAze&M?7qYu1Mo^#&U z7QMWy0XHG&qpa$K-&=|aDaP4kN6>?|U&e(xSeOfk8bM0$dm~A0>a#J7U7;=tEA(uJg7gyND81&{ zPqg7_0(;_k-28w%nj`}glRE#)DOz2;IjSQfEFVXD^s3IFi8n+w zq|f8trT@gxsUPvc?QOSkHpH{9h-mzHgj<-f#C=XG}B05;JDReJI zV-qj&^}TxsbmF{=D{%RDrn7~rlFD-GozkOt% zq2}ktGMqH#_Q0XV7_QQQMuC8 zG`WcKJDB}UPnw~y)@7(|OuI`iL$(?)k*57^| zxvW=P&0U}2Q#5RJkmdpZ#|gRTe$)knpn#F~+i;`7dzV_*`e&5je1xdq|Bmg^{OfCD zkWYDZ6Kk^p>PP+_>VxmMzr`Whz5w`P3+EIYz3g;%4xD|%w1O|bhGPxQf>A>U11Q1K zae(7ym3><>&8^@B@_uipyIes}^G<`cn5+RrD|jgYIW#Lj*Ngqf)mF7d!wOtod>%QtHP zH%U$|ydM{KsJchX!w-EJSKtCYmdSziic_XVN4mCJVEC@j136|^HZ0?9h&sm+kT7ZR zh;xUCz8gYf6|+?ASC*biD*E^R#%kO=X?D?3+yE&2&aPBpPL7rDSkfF7OB43hcPVD2 z2wvR!MH{@OZGgF56>EGo!NwX(an(8eFyUSt=;3qEOW|&C=3NvagUUW3-SM!G92Z&W z_O3Buq+VV-yHsaS<4<~An`Lb!%DN}0RRV^G6#VRW3;a(^{67FFz=3}h^7T>-x2ArM zH|eL)3}JO_SmSFZPh2-|)YG1f`a+Ra#f*75xd0e>0`EO-3k65eWKD1<`}p%u!al^J zhRMe$`7z0B*Sl7y9nv;8$WFu6(lAdNIvfL+iL{c}(ZTL0FOs_kyuD0a$I~g6E8T(t z6Y22XBo0QC%;}EB(3;~#tUe4VCb8jE9}k9Sk3S*;Uh8F&$OA>L6pkM z8n}{rBcI4bci^;L%tWjPP&vst0p-CA;RyzYos@Q_{Yg?60|m>E&uKX)qgX3^cOspV zKJo3u7r9_bonk-UpBpfly*-WS7xG;xWG<2kAcy*t=(|=Sd^Zr z=f#ygR6>8J`C^0=0!Xa|sCPhWN{e-b{a+gyFo~R}DuqP}0zVfnGT18}0Z9YSX7AQi zYJ_H-J>`HnPSGD9Ck8>Igxi>Lng{O-q8uVW{+zB+lDRH1oPuIcXuvS)67thc8U^wy z^BS(%R*buo*$;2%4L{RUDY%^j^S*Luk?oafIZ#-;HtuyFHm*Wc1$vTK!z( literal 0 HcmV?d00001 diff --git a/src/Cli/src/Properties/launchSettings.json b/src/Cli/src/Properties/launchSettings.json new file mode 100644 index 0000000000..4ef54e5ecd --- /dev/null +++ b/src/Cli/src/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Cli": { + "commandName": "Project", + "commandLineArgs": "-p:generateConfigFiles=true" + } + } +} \ No newline at end of file From 2fa812d1b093ae86efa8d09b819e22fd2559db73 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 8 Nov 2022 00:15:44 -0800 Subject: [PATCH 05/31] Remove addition of launchSettings --- src/Cli/src/Properties/launchSettings.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/Cli/src/Properties/launchSettings.json diff --git a/src/Cli/src/Properties/launchSettings.json b/src/Cli/src/Properties/launchSettings.json deleted file mode 100644 index 4ef54e5ecd..0000000000 --- a/src/Cli/src/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "Cli": { - "commandName": "Project", - "commandLineArgs": "-p:generateConfigFiles=true" - } - } -} \ No newline at end of file From 71f7c3d692b8da193949ec7456ecf68d65a10f7e Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 8 Nov 2022 00:18:45 -0800 Subject: [PATCH 06/31] Add a relationship directive on input field node --- .../Directives/RelationshipDirective.cs | 37 +++++++++++++++---- .../Queries/InputTypeBuilder.cs | 12 ++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs b/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs index 0788622ca8..d3ab4f9153 100644 --- a/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs +++ b/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs @@ -13,7 +13,7 @@ protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor.Name(DirectiveName) .Description("A directive to indicate the relationship between two tables") - .Location(DirectiveLocation.FieldDefinition); + .Location(DirectiveLocation.FieldDefinition | DirectiveLocation.InputFieldDefinition); descriptor.Argument("target") .Type() @@ -25,10 +25,10 @@ protected override void Configure(IDirectiveTypeDescriptor descriptor) } /// - /// Gets the target object type name for a field with a relationship directive. + /// Gets the target object type name for a ifield with a relationship directive. /// - /// The field that has a relationship directive defined. - /// The name of the GraphQL object type that the relationship targets. If no relationship is defined, the object type of the field is returned. + /// The ifield that has a relationship directive defined. + /// The name of the GraphQL object type that the relationship targets. If no relationship is defined, the object type of the ifield is returned. public static string Target(FieldDefinitionNode field) { DirectiveNode? directive = field.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); @@ -43,24 +43,47 @@ public static string Target(FieldDefinitionNode field) return (string)arg.Value.Value!; } + /// + /// Gets the target object type name for an input ifield with a relationship directive. + /// + /// The input field that is expected to have a relationship directive defined on it. + /// The name of the target object if the relationship is found, null otherwise. + public static string? Target(InputField ifield) + { + Directive? directive = (Directive?)ifield.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); + DirectiveNode? directiveNode = directive?.ToNode(); + ArgumentNode? arg = directiveNode?.Arguments.First(a => a.Name.Value == "target"); + + return (string?)arg?.Value.Value; + } + /// /// Gets the cardinality of the relationship. /// - /// The field that has a relationship directive defined. + /// The ifield that has a relationship directive defined. /// Relationship cardinality - /// Thrown if the field does not have a defined relationship. + /// Thrown if the ifield does not have a defined relationship. public static Cardinality Cardinality(FieldDefinitionNode field) { DirectiveNode? directive = field.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); if (directive == null) { - throw new ArgumentException("The specified field does not have a relationship directive defined."); + throw new ArgumentException("The specified ifield does not have a relationship directive defined."); } ArgumentNode arg = directive.Arguments.First(a => a.Name.Value == "cardinality"); return Enum.Parse((string)arg.Value.Value!); } + + /// + /// Retrieve the relationship directive defined on the given ifield definition node. + /// + public static DirectiveNode? GetDirective(FieldDefinitionNode field) + { + DirectiveNode? directive = field.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); + return directive; + } } } diff --git a/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs b/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs index 4dd9c5a6f8..b1c9b25541 100644 --- a/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs @@ -137,6 +137,14 @@ private static List GenerateFilterInputFieldsForBuiltI { string targetEntityName = RelationshipDirectiveType.Target(field); + DirectiveNode? relationshipDirective = + RelationshipDirectiveType.GetDirective(field); + List directives = new(); + if (relationshipDirective is not null) + { + directives.Add(relationshipDirective); + } + inputFields.Add( new( location: null, @@ -144,10 +152,8 @@ private static List GenerateFilterInputFieldsForBuiltI new StringValueNode($"Filter options for {field.Name}"), new NamedTypeNode(GenerateObjectInputFilterName(targetEntityName)), defaultValue: null, - new List()) - ); + directives: directives)); } - } return inputFields; From f1bd4f805f6fd42caff839ce18436bb80dca4189 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 8 Nov 2022 00:19:31 -0800 Subject: [PATCH 07/31] Add 2 options outline for SQL filtering --- src/Service/Models/GraphQLFilterParsers.cs | 49 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index bf2c0fae21..d3354158e9 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; using HotChocolate.Resolvers; @@ -56,7 +57,6 @@ public static Predicate Parse( bool fieldIsOr = string.Equals(name, $"{PredicateOperation.OR}", StringComparison.OrdinalIgnoreCase); InputObjectType filterInputObjectType = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentObject.Fields[name]); - if (fieldIsAnd || fieldIsOr) { PredicateOperation op = fieldIsAnd ? PredicateOperation.AND : PredicateOperation.OR; @@ -80,10 +80,49 @@ public static Predicate Parse( if (!IsSingularType(filterInputObjectType.Name)) { - return Parse(ctx, - filterArgumentObject.Fields[name], - subfields, - schemaName, sourceName + "." + name, sourceAlias + "." + name, sourceDefinition, processLiterals); + // For SQL, + if (sourceDefinition.PrimaryKey.Count != 0) + { + // if there are primary keys on the source, we need to perform a join + // between the source and the non-scalar filter entity. + InputField filterField = filterArgumentObject.Fields[name]; + + string? targetEntityForFilter; + _ = RelationshipDirectiveType.Target(filterField); + + /* if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) + { + EntityName = modelName; + } + + DatabaseObject.SchemaName = sqlMetadataProvider.GetSchemaName(EntityName); + DatabaseObject.Name = sqlMetadataProvider.GetDatabaseObjectName(EntityName); + Predicate predicateOnExists = Parse( + ctx, + argumentSchema: filterArgumentObject.Fields[name], + fields: subfields, + );*/ + // Recursively parse and obtain the predicates for the Exists clause subquery + // Create a SqlQueryStructure as the predicate operand of Exists predicate with no order by, no limit, select 1 + // - with predicates = the predicate obtained from recursively parsing + // Add JoinPredicates to the subquery query structure so a predicate connecting + // the outer table is added to the where clause of subquery + // make chained predicate using this exists predicate and return. + // Handle Exist clause while Building each of the Predicates. + // Build the Exists predicate subquery using special Build of SqlQueryStructure + + // Recursively parse and obtain the predicates for the join subquery to add to the Joins property of type List + // - with predicates = the predicate obtained from recursively parsing and a predicate relating the two entities + // continue with rest of the filters + // Handle join clause while Building the original query structure + } + else + { + return Parse(ctx, + filterArgumentObject.Fields[name], + subfields, + schemaName, sourceName + "." + name, sourceAlias + "." + name, sourceDefinition, processLiterals); + } } else { From ee6485540ee56c7a17448b73e31f906af0ad5c21 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 8 Nov 2022 20:05:56 -0800 Subject: [PATCH 08/31] Refactoring changes for accommodating nested filters for SQL --- .../Authorization/AuthorizationHelpers.cs | 2 +- .../Unittests/RequestValidatorUnitTests.cs | 2 +- .../Unittests/RestServiceUnitTests.cs | 2 +- src/Service/Models/GraphQLFilterParsers.cs | 132 +++++++----------- src/Service/Parsers/EdmModelBuilder.cs | 6 +- src/Service/Parsers/RequestParser.cs | 2 +- src/Service/Resolvers/BaseQueryStructure.cs | 62 ++++++++ src/Service/Resolvers/BaseSqlQueryBuilder.cs | 10 +- src/Service/Resolvers/CosmosQueryEngine.cs | 7 +- src/Service/Resolvers/CosmosQueryStructure.cs | 49 ++++--- src/Service/Resolvers/MsSqlQueryBuilder.cs | 2 +- src/Service/Resolvers/MySqlQueryBuilder.cs | 2 +- src/Service/Resolvers/PostgresQueryBuilder.cs | 6 +- .../BaseSqlQueryStructure.cs | 53 +------ .../SqlDeleteQueryStructure.cs | 2 +- .../SqlInsertQueryStructure.cs | 2 +- .../Sql Query Structures/SqlQueryStructure.cs | 83 ++++++----- .../SqlUpdateQueryStructure.cs | 4 +- .../SqlUpsertQueryStructure.cs | 2 +- src/Service/Resolvers/SqlQueryEngine.cs | 25 +++- .../CosmosSqlMetadataProvider.cs | 8 +- .../MetadataProviders/ISqlMetadataProvider.cs | 24 ++++ .../MsSqlMetadataProvider.cs | 2 +- .../MySqlMetadataProvider.cs | 2 +- .../PostgreSqlMetadataProvider.cs | 2 +- .../MetadataProviders/SqlMetadataProvider.cs | 27 +++- 26 files changed, 291 insertions(+), 229 deletions(-) diff --git a/src/Service.Tests/Authorization/AuthorizationHelpers.cs b/src/Service.Tests/Authorization/AuthorizationHelpers.cs index db35b0bec3..7b0f1c5e15 100644 --- a/src/Service.Tests/Authorization/AuthorizationHelpers.cs +++ b/src/Service.Tests/Authorization/AuthorizationHelpers.cs @@ -170,7 +170,7 @@ public static Dictionary> CreateColumnMapping /// Without use of delegate the out param will /// not be populated with the correct value. /// This delegate is for the callback used - /// with the mocked SqlMetadataProvider. + /// with the mocked MetadataProvider. /// /// Name of entity. /// Exposed field name. diff --git a/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs b/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs index 11a0c2cb56..ad00fc8e4c 100644 --- a/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs +++ b/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs @@ -438,7 +438,7 @@ private static void PerformRequestParserPrimaryKeyTest( /// Without use of delegate the out param will /// not be populated with the correct value. /// This delegate is for the callback used - /// with the mocked SqlMetadataProvider. + /// with the mocked MetadataProvider. /// /// Name of entity. /// Exposed field name. diff --git a/src/Service.Tests/Unittests/RestServiceUnitTests.cs b/src/Service.Tests/Unittests/RestServiceUnitTests.cs index e35eef6b95..b88a3899f4 100644 --- a/src/Service.Tests/Unittests/RestServiceUnitTests.cs +++ b/src/Service.Tests/Unittests/RestServiceUnitTests.cs @@ -184,7 +184,7 @@ public static void InitializeTest(string path, string entityName) /// Without use of delegate the out param will /// not be populated with the correct value. /// This delegate is for the callback used - /// with the mocked SqlMetadataProvider. + /// with the mocked MetadataProvider. /// /// The entity path. /// Name of entity. diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index 8e36204277..ce89c6c121 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; +using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; using HotChocolate.Resolvers; @@ -13,29 +13,42 @@ namespace Azure.DataApiBuilder.Service.Models /// /// Contains methods to parse a GQL filter parameter /// - public static class GQLFilterParser + public class GQLFilterParser { public static readonly string NullStringValue = "NULL"; + private readonly ISqlMetadataProvider _metadataProvider; + + /// + /// Constructor for the filter parser. + /// + /// The metadata provider of the respective database. + public GQLFilterParser(ISqlMetadataProvider metadataProvider) + { + _metadataProvider = metadataProvider; + } + /// /// Parse a predicate for a *FilterInput input type /// /// The GraphQL context, used to get the query variables /// An IInputField object which describes the schema of the filter argument /// The fields in the *FilterInput being processed - /// The source alias underlyin the *FilterInput being processed - /// Definition of the table/view underlying the *FilterInput being processed - /// Parametrizes literals before they are written in string predicate operands - public static Predicate Parse( + /// The query structure for the entity being filtered providing + /// the source alias of the underlying *FilterInput being processed, + /// source definition of the table/view of the underlying *FilterInput being processed, + /// and the function that parametrizes literals before they are written in string predicate operands. + public Predicate Parse( IMiddlewareContext ctx, IInputField filterArgumentSchema, List fields, - string schemaName, - string sourceName, - string sourceAlias, - SourceDefinition sourceDefinition, - Func processLiterals) + BaseQueryStructure queryStructure) { + string schemaName = queryStructure.DatabaseObject.SchemaName; + string sourceName = queryStructure.DatabaseObject.Name; + string sourceAlias = queryStructure.SourceAlias; + SourceDefinition sourceDefinition = queryStructure.GetUnderlyingSourceDefinition(); + InputObjectType filterArgumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentSchema); List predicates = new(); @@ -67,12 +80,8 @@ public static Predicate Parse( argumentSchema: filterArgumentObject.Fields[name], filterArgumentSchema: filterArgumentSchema, otherPredicates, - schemaName, - sourceName, - sourceAlias, - sourceDefinition, - op, - processLiterals))); + queryStructure, + op))); } else { @@ -80,65 +89,26 @@ public static Predicate Parse( if (!IsScalarType(filterInputObjectType.Name)) { - // For SQL, - if (sourceDefinition.PrimaryKey.Count != 0) - { - // if there are primary keys on the source, we need to perform a join - // between the source and the non-scalar filter entity. - InputField filterField = filterArgumentObject.Fields[name]; - - string? targetEntityForFilter; - _ = RelationshipDirectiveType.Target(filterField); - - /* if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) - { - EntityName = modelName; - } - - DatabaseObject.SchemaName = sqlMetadataProvider.GetSchemaName(EntityName); - DatabaseObject.Name = sqlMetadataProvider.GetDatabaseObjectName(EntityName); - Predicate predicateOnExists = Parse( - ctx, - argumentSchema: filterArgumentObject.Fields[name], - fields: subfields, - );*/ - // Recursively parse and obtain the predicates for the Exists clause subquery - // Create a SqlQueryStructure as the predicate operand of Exists predicate with no order by, no limit, select 1 - // - with predicates = the predicate obtained from recursively parsing - // Add JoinPredicates to the subquery query structure so a predicate connecting - // the outer table is added to the where clause of subquery - // make chained predicate using this exists predicate and return. - // Handle Exist clause while Building each of the Predicates. - // Build the Exists predicate subquery using special Build of SqlQueryStructure - - // Recursively parse and obtain the predicates for the join subquery to add to the Joins property of type List - // - with predicates = the predicate obtained from recursively parsing and a predicate relating the two entities - // continue with rest of the filters - // Handle join clause while Building the original query structure - } - else - { - predicates.Push(new PredicateOperand(Parse(ctx, - filterArgumentObject.Fields[name], - subfields, - schemaName, - sourceName + "." + name, - sourceAlias + "." + name, - sourceDefinition, - processLiterals))); - } + queryStructure.DatabaseObject.Name = sourceName + "." + name; + queryStructure.SourceAlias = sourceName + "." + name; + predicates.Push(new PredicateOperand(Parse(ctx, + filterArgumentObject.Fields[name], + subfields, + queryStructure))); } else { - predicates.Push(new PredicateOperand(ParseScalarType( - ctx, - argumentSchema: filterArgumentObject.Fields[name], - name, - subfields, - schemaName, - sourceName, - sourceAlias, - processLiterals))); + predicates.Push( + new PredicateOperand( + ParseScalarType( + ctx, + argumentSchema: filterArgumentObject.Fields[name], + name, + subfields, + schemaName, + sourceName, + sourceAlias, + queryStructure.MakeParamWithValue))); } } } @@ -196,17 +166,13 @@ private static Predicate ParseScalarType( /// Definition of the table/view underlying the *FilterInput being processed /// The operation (and or or) /// Parametrizes literals before they are written in string predicate operands - private static Predicate ParseAndOr( + private Predicate ParseAndOr( IMiddlewareContext ctx, IInputField argumentSchema, IInputField filterArgumentSchema, List fields, - string schemaName, - string tableName, - string tableAlias, - SourceDefinition sourceDefinition, - PredicateOperation op, - Func processLiterals) + BaseQueryStructure baseQuery, + PredicateOperation op) { if (fields.Count == 0) { @@ -227,7 +193,11 @@ private static Predicate ParseAndOr( } List subfields = (List)fieldValue; - operands.Add(new PredicateOperand(Parse(ctx, filterArgumentSchema, subfields, schemaName, tableName, tableAlias, sourceDefinition, processLiterals))); + operands.Add(new PredicateOperand( + Parse(ctx, + filterArgumentSchema, + subfields, + baseQuery))); } return MakeChainPredicate(operands, op); diff --git a/src/Service/Parsers/EdmModelBuilder.cs b/src/Service/Parsers/EdmModelBuilder.cs index ea07f87a7b..522e1aeee7 100644 --- a/src/Service/Parsers/EdmModelBuilder.cs +++ b/src/Service/Parsers/EdmModelBuilder.cs @@ -26,7 +26,7 @@ public IEdmModel GetModel() /// /// Build the model from the provided schema. /// - /// The SqlMetadataProvider holds the objects needed + /// The MetadataProvider holds the objects needed /// to build the correct model. /// An EdmModelBuilder that can be used to get a model. public EdmModelBuilder BuildModel(ISqlMetadataProvider sqlMetadataProvider) @@ -38,7 +38,7 @@ public EdmModelBuilder BuildModel(ISqlMetadataProvider sqlMetadataProvider) /// /// Add the entity types found in the schema to the model /// - /// The SqlMetadataProvider holds the objects needed + /// The MetadataProvider holds the objects needed /// to build the correct model. /// this model builder private EdmModelBuilder BuildEntityTypes(ISqlMetadataProvider sqlMetadataProvider) @@ -146,7 +146,7 @@ SourceDefinition sourceDefinition /// /// Add the entity sets contained within the schema to container. /// - /// The SqlMetadataProvider holds the objects needed + /// The MetadataProvider holds the objects needed /// to build the correct model. /// this model builder private EdmModelBuilder BuildEntitySets(ISqlMetadataProvider sqlMetadataProvider) diff --git a/src/Service/Parsers/RequestParser.cs b/src/Service/Parsers/RequestParser.cs index af1cf9d4e6..ce093c89c5 100644 --- a/src/Service/Parsers/RequestParser.cs +++ b/src/Service/Parsers/RequestParser.cs @@ -92,7 +92,7 @@ public static void ParsePrimaryKey(string primaryKeyRoute, RestRequestContext co /// so any instance of a null key will result in a bad request. /// /// The RestRequestContext holding the major components of the query. - /// The SqlMetadataProvider holds many of the components needed to parse the query. + /// The MetadataProvider holds many of the components needed to parse the query. /// public static void ParseQueryString(RestRequestContext context, ISqlMetadataProvider sqlMetadataProvider) { diff --git a/src/Service/Resolvers/BaseQueryStructure.cs b/src/Service/Resolvers/BaseQueryStructure.cs index b975c459bf..17a5e969d0 100644 --- a/src/Service/Resolvers/BaseQueryStructure.cs +++ b/src/Service/Resolvers/BaseQueryStructure.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; +using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using Azure.DataApiBuilder.Service.Models; +using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; using HotChocolate.Types; @@ -9,6 +11,24 @@ namespace Azure.DataApiBuilder.Service.Resolvers { public class BaseQueryStructure { + /// + /// The Entity associated with this query. + /// + public string EntityName { get; protected set; } + + /// + /// The alias of the main entity to be queried. + /// + public virtual string SourceAlias { get; set; } + + protected ISqlMetadataProvider MetadataProvider { get; } + + /// + /// The DatabaseObject associated with the entity, represents the + /// databse object to be queried. + /// + public DatabaseObject DatabaseObject { get; protected set; } = null!; + /// /// The columns which the query selects /// @@ -31,13 +51,39 @@ public class BaseQueryStructure /// public List Predicates { get; } + public GQLFilterParser GraphQLFilterParser { get; protected set; } + public BaseQueryStructure( + ISqlMetadataProvider metadataProvider, + GQLFilterParser gQLFilterParser, + string entityName, IncrementingInteger? counter = null) { Columns = new(); Predicates = new(); Parameters = new(); Counter = counter ?? new IncrementingInteger(); + MetadataProvider = metadataProvider; + GraphQLFilterParser = gQLFilterParser; + + // Default the alias to the empty string since this base construtor + // 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 contructor. + SourceAlias = string.Empty; + + if (!string.IsNullOrEmpty(entityName)) + { + EntityName = entityName; + DatabaseObject = MetadataProvider.EntityToDatabaseObject[entityName]; + } + else + { + EntityName = string.Empty; + // This is the cosmos db metadata scenario + // where the table name is the Source Alias i.e. container alias + DatabaseObject = new DatabaseTable(string.Empty, SourceAlias); + } } /// @@ -51,6 +97,22 @@ public string MakeParamWithValue(object? value) return paramName; } + /// + /// Creates a unique table alias. + /// + public string CreateTableAlias() + { + return $"table{Counter.Next()}"; + } + + /// + /// Returns the SourceDefinitionDefinition for the entity(table/view) of this query. + /// + public SourceDefinition GetUnderlyingSourceDefinition() + { + return MetadataProvider.GetSourceDefinition(EntityName); + } + /// /// Extracts the *Connection.items query field from the *Connection query field /// diff --git a/src/Service/Resolvers/BaseSqlQueryBuilder.cs b/src/Service/Resolvers/BaseSqlQueryBuilder.cs index 3eb87d9195..f1e0038585 100644 --- a/src/Service/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Service/Resolvers/BaseSqlQueryBuilder.cs @@ -113,14 +113,14 @@ private static string GetComparisonFromDirection(OrderBy direction) /// /// Build column as /// [{tableAlias}].[{ColumnName}] - /// or if TableAlias is empty, as + /// or if SourceAlias is empty, as /// [{schema}].[{table}].[{ColumnName}] /// or if schema is empty, as /// [{table}].[{ColumnName}] /// protected virtual string Build(Column column) { - // If the table alias is not empty, we return [{TableAlias}].[{Column}] + // If the table alias is not empty, we return [{SourceAlias}].[{Column}] if (!string.IsNullOrEmpty(column.TableAlias)) { return $"{QuoteIdentifier(column.TableAlias)}.{QuoteIdentifier(column.ColumnName)}"; @@ -139,8 +139,8 @@ protected virtual string Build(Column column) /// /// Build orderby column as - /// {TableAlias}.{ColumnName} {direction} - /// If TableAlias is null + /// {SourceAlias}.{ColumnName} {direction} + /// If SourceAlias is null /// {ColumnName} {direction} /// protected virtual string Build(OrderByColumn column, bool printDirection = true) @@ -282,7 +282,7 @@ protected string Build(List predicates, string separator = " AND ") /// /// Write the join in sql - /// INNER JOIN {TableName} AS {TableAlias} ON {JoinPredicates} + /// INNER JOIN {TableName} AS {SourceAlias} ON {JoinPredicates} /// protected string Build(SqlJoinStructure join) { diff --git a/src/Service/Resolvers/CosmosQueryEngine.cs b/src/Service/Resolvers/CosmosQueryEngine.cs index 9a6060f1c1..e8d770817b 100644 --- a/src/Service/Resolvers/CosmosQueryEngine.cs +++ b/src/Service/Resolvers/CosmosQueryEngine.cs @@ -25,17 +25,20 @@ public class CosmosQueryEngine : IQueryEngine private readonly CosmosClientProvider _clientProvider; private readonly ISqlMetadataProvider _metadataStoreProvider; private readonly CosmosQueryBuilder _queryBuilder; + private readonly GQLFilterParser _gQLFilterParser; // // Constructor. // public CosmosQueryEngine( CosmosClientProvider clientProvider, - ISqlMetadataProvider metadataStoreProvider) + ISqlMetadataProvider metadataStoreProvider, + GQLFilterParser gQLFilterParser) { _clientProvider = clientProvider; _metadataStoreProvider = metadataStoreProvider; _queryBuilder = new CosmosQueryBuilder(); + _gQLFilterParser = gQLFilterParser; } /// @@ -49,7 +52,7 @@ public async Task> ExecuteAsync(IMiddlewareContex // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down - CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider); + CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider, _gQLFilterParser); string requestContinuation = null; string queryString = _queryBuilder.Build(structure); diff --git a/src/Service/Resolvers/CosmosQueryStructure.cs b/src/Service/Resolvers/CosmosQueryStructure.cs index 4040f8d1d1..20f2027ff8 100644 --- a/src/Service/Resolvers/CosmosQueryStructure.cs +++ b/src/Service/Resolvers/CosmosQueryStructure.cs @@ -16,11 +16,12 @@ namespace Azure.DataApiBuilder.Service.Resolvers public class CosmosQueryStructure : BaseQueryStructure { private readonly IMiddlewareContext _context; - private readonly ISqlMetadataProvider _metadataProvider; + private readonly string _containerAlias = "c"; + + public override string SourceAlias { get => base.SourceAlias; set => base.SourceAlias = value; } public bool IsPaginated { get; internal set; } - private readonly string _containerAlias = "c"; public string Container { get; internal set; } public string Database { get; internal set; } public string? Continuation { get; internal set; } @@ -31,11 +32,12 @@ public class CosmosQueryStructure : BaseQueryStructure public CosmosQueryStructure( IMiddlewareContext context, IDictionary parameters, - ISqlMetadataProvider metadataProvider) - : base() + ISqlMetadataProvider metadataProvider, + GQLFilterParser gQLFilterParser) + : base(metadataProvider, gQLFilterParser, entityName: string.Empty) { - _metadataProvider = metadataProvider; _context = context; + SourceAlias = _containerAlias; Init(parameters); } @@ -57,27 +59,27 @@ private void Init(IDictionary queryParams) if (fieldNode != null) { Columns.AddRange(fieldNode.SelectionSet!.Selections.Select(x => new LabelledColumn(tableSchema: string.Empty, - tableName: _containerAlias, + tableName: SourceAlias, columnName: string.Empty, label: x.GetNodes().First().ToString()))); } ObjectType realType = GraphQLUtils.UnderlyingGraphQLEntityType(underlyingType.Fields[QueryBuilder.PAGINATION_FIELD_NAME].Type); - string entityName = _metadataProvider.GetEntityName(realType.Name); + string entityName = MetadataProvider.GetEntityName(realType.Name); - Database = _metadataProvider.GetSchemaName(entityName); - Container = _metadataProvider.GetDatabaseObjectName(entityName); + Database = MetadataProvider.GetSchemaName(entityName); + Container = MetadataProvider.GetDatabaseObjectName(entityName); } else { Columns.AddRange(selection.SyntaxNode.SelectionSet!.Selections.Select(x => new LabelledColumn(tableSchema: string.Empty, - tableName: _containerAlias, + tableName: SourceAlias, columnName: string.Empty, label: x.GetNodes().First().ToString()))); - string entityName = _metadataProvider.GetEntityName(underlyingType.Name); + string entityName = MetadataProvider.GetEntityName(underlyingType.Name); - Database = _metadataProvider.GetSchemaName(entityName); - Container = _metadataProvider.GetDatabaseObjectName(entityName); + Database = MetadataProvider.GetSchemaName(entityName); + Container = MetadataProvider.GetDatabaseObjectName(entityName); } // first and after will not be part of query parameters. They will be going into headers instead. @@ -119,15 +121,18 @@ private void Init(IDictionary queryParams) if (filterObject != null) { List filterFields = (List)filterObject; - Predicates.Add(GQLFilterParser.Parse( - _context, - filterArgumentSchema: selection.Field.Arguments[QueryBuilder.FILTER_FIELD_NAME], - fields: filterFields, - schemaName: string.Empty, - sourceName: _containerAlias, - sourceAlias: _containerAlias, - sourceDefinition: new SourceDefinition(), - processLiterals: MakeParamWithValue)); + Predicates.Add( + GraphQLFilterParser.Parse( + _context, + filterArgumentSchema: selection.Field.Arguments[QueryBuilder.FILTER_FIELD_NAME], + fields: filterFields, + queryStructure: this)); + + // after parsing all the graphql filters, + // reset the source alias and object name to the generic container alias + // since these may potentially be updated due to the presence of nested filters. + SourceAlias = _containerAlias; + DatabaseObject.Name = _containerAlias; } } else diff --git a/src/Service/Resolvers/MsSqlQueryBuilder.cs b/src/Service/Resolvers/MsSqlQueryBuilder.cs index 2ed3dde89f..af2342d2d9 100644 --- a/src/Service/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Service/Resolvers/MsSqlQueryBuilder.cs @@ -28,7 +28,7 @@ public string Build(SqlQueryStructure structure) { string dataIdent = QuoteIdentifier(SqlQueryStructure.DATA_IDENT); string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + - $"AS {QuoteIdentifier($"{structure.TableAlias}")}{Build(structure.Joins)}"; + $"AS {QuoteIdentifier($"{structure.SourceAlias}")}{Build(structure.Joins)}"; fromSql += string.Join( "", diff --git a/src/Service/Resolvers/MySqlQueryBuilder.cs b/src/Service/Resolvers/MySqlQueryBuilder.cs index 98c0c1fe01..95cb954301 100644 --- a/src/Service/Resolvers/MySqlQueryBuilder.cs +++ b/src/Service/Resolvers/MySqlQueryBuilder.cs @@ -29,7 +29,7 @@ public override string QuoteIdentifier(string ident) /// public string Build(SqlQueryStructure structure) { - string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.Name)} AS {QuoteIdentifier(structure.TableAlias)}{Build(structure.Joins)}"; + string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.Name)} AS {QuoteIdentifier(structure.SourceAlias)}{Build(structure.Joins)}"; fromSql += string.Join("", structure.JoinQueries.Select(x => $" LEFT OUTER JOIN LATERAL ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)} ON TRUE")); string predicates = JoinPredicateStrings( diff --git a/src/Service/Resolvers/PostgresQueryBuilder.cs b/src/Service/Resolvers/PostgresQueryBuilder.cs index 52170c25fb..41b7d96146 100644 --- a/src/Service/Resolvers/PostgresQueryBuilder.cs +++ b/src/Service/Resolvers/PostgresQueryBuilder.cs @@ -29,7 +29,7 @@ public override string QuoteIdentifier(string ident) public string Build(SqlQueryStructure structure) { string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + - $"AS {QuoteIdentifier(structure.TableAlias)}{Build(structure.Joins)}"; + $"AS {QuoteIdentifier(structure.SourceAlias)}{Build(structure.Joins)}"; fromSql += string.Join("", structure.JoinQueries.Select(x => $" LEFT OUTER JOIN LATERAL ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)} ON TRUE")); string predicates = JoinPredicateStrings( @@ -141,12 +141,12 @@ private string BuildListOfLabels(List labelledColumns) /// /// Build column as /// "{tableAlias}"."{ColumnName}" - /// or if TableAlias is empty, as + /// or if SourceAlias is empty, as /// "{ColumnName}" /// protected override string Build(Column column) { - // If the table alias is not empty, we return [{TableAlias}].[{Column}] + // If the table alias is not empty, we return [{SourceAlias}].[{Column}] if (!string.IsNullOrEmpty(column.TableAlias)) { return $"{QuoteIdentifier(column.TableAlias)}.{QuoteIdentifier(column.ColumnName)}"; diff --git a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index cac10f882a..3b2426cef3 100644 --- a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -22,24 +22,6 @@ namespace Azure.DataApiBuilder.Service.Resolvers /// public abstract class BaseSqlQueryStructure : BaseQueryStructure { - protected ISqlMetadataProvider SqlMetadataProvider { get; } - - /// - /// The Entity associated with this query. - /// - public string EntityName { get; protected set; } - - /// - /// The DatabaseObject associated with the entity, represents the - /// databse object to be queried. - /// - public DatabaseObject DatabaseObject { get; } - - /// - /// The alias of the main table to be queried. - /// - public string TableAlias { get; protected set; } - /// /// FilterPredicates is a string that represents the filter portion of our query /// in the WHERE Clause. This is generated specifically from the $filter portion @@ -55,27 +37,12 @@ public abstract class BaseSqlQueryStructure : BaseQueryStructure public BaseSqlQueryStructure( ISqlMetadataProvider sqlMetadataProvider, + GQLFilterParser gQLFilterParser, string entityName, IncrementingInteger? counter = null) - : base(counter) + : base(sqlMetadataProvider, gQLFilterParser, entityName, counter) { - SqlMetadataProvider = sqlMetadataProvider; - if (!string.IsNullOrEmpty(entityName)) - { - EntityName = entityName; - DatabaseObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; - } - else - { - EntityName = string.Empty; - DatabaseObject = new DatabaseTable(); - } - // Default the alias to the empty string since this base construtor - // is called for requests other than Find operations. We only use - // TableAlias for Find, so we leave empty here and then populate - // in the Find specific contructor. - TableAlias = string.Empty; } /// @@ -133,20 +100,12 @@ public Type GetColumnSystemType(string columnName) } } - /// - /// Returns the SourceDefinitionDefinition for the entity(table/view) of this query. - /// - protected SourceDefinition GetUnderlyingSourceDefinition() - { - return SqlMetadataProvider.GetSourceDefinition(EntityName); - } - /// /// Return the StoredProcedureDefinition associated with this database object /// protected StoredProcedureDefinition GetUnderlyingStoredProcedureDefinition() { - return SqlMetadataProvider.GetStoredProcedureDefinition(EntityName); + return MetadataProvider.GetStoredProcedureDefinition(EntityName); } /// @@ -176,7 +135,7 @@ protected List GenerateOutputColumns() List outputColumns = new(); foreach (string columnName in GetUnderlyingSourceDefinition().Columns.Keys) { - if (!SqlMetadataProvider.TryGetExposedColumnName( + if (!MetadataProvider.TryGetExposedColumnName( entityName: EntityName, backingFieldName: columnName, out string? exposedName)) @@ -189,7 +148,7 @@ protected List GenerateOutputColumns() tableName: DatabaseObject.Name, columnName: columnName, label: exposedName!, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } return outputColumns; @@ -352,7 +311,7 @@ internal static List GetSubArgumentNamesFromGQLMutArguments /// Thrown when the OData visitor traversal fails. Possibly due to malformed clause. public void ProcessOdataClause(FilterClause odataClause) { - ODataASTVisitor visitor = new(this, this.SqlMetadataProvider); + ODataASTVisitor visitor = new(this, this.MetadataProvider); try { DbPolicyPredicates = GetFilterPredicatesFromOdataClause(odataClause, visitor); diff --git a/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs index 44b6e5c736..9d14477336 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs @@ -33,7 +33,7 @@ public SqlDeleteStructure( } // primary keys used as predicates - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); if (primaryKeys.Contains(backingColumn!)) { Predicates.Add(new Predicate( diff --git a/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs index 82b6656fab..4f4ec9cf2a 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs @@ -53,7 +53,7 @@ public SqlInsertStructure( foreach (KeyValuePair param in mutationParams) { - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); PopulateColumnsAndParams(backingColumn!, param.Value); } } diff --git a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 9b46bc5ed3..7a4d5e78af 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -107,7 +107,8 @@ public SqlQueryStructure( IDictionary queryParams, ISqlMetadataProvider sqlMetadataProvider, IAuthorizationResolver authorizationResolver, - RuntimeConfigProvider runtimeConfigProvider) + RuntimeConfigProvider runtimeConfigProvider, + GQLFilterParser gQLFilterParser) // This constructor simply forwards to the more general constructor // that is used to create GraphQL queries. We give it some values // that make sense for the outermost query. @@ -121,7 +122,8 @@ public SqlQueryStructure( // create the IncrementingInteger that will be shared between // all subqueries in this query. new IncrementingInteger(), - runtimeConfigProvider) + runtimeConfigProvider, + gQLFilterParser) { // support identification of entities by primary key when query is non list type nor paginated // only perform this action for the outermost query as subqueries shouldn't provide primary key search @@ -138,13 +140,15 @@ public SqlQueryStructure( public SqlQueryStructure( RestRequestContext context, ISqlMetadataProvider sqlMetadataProvider, - RuntimeConfigProvider runtimeConfigProvider) : - this(sqlMetadataProvider, - new IncrementingInteger(), - entityName: context.EntityName) + RuntimeConfigProvider runtimeConfigProvider, + GQLFilterParser gQLFilterParser) : + this(sqlMetadataProvider, + new IncrementingInteger(), + gQLFilterParser, + entityName: context.EntityName) { IsListQuery = context.IsMany; - TableAlias = $"{DatabaseObject.SchemaName}_{DatabaseObject.Name}"; + SourceAlias = $"{DatabaseObject.SchemaName}_{DatabaseObject.Name}"; AddFields(context, sqlMetadataProvider); if (Columns.Count == 0) { @@ -175,9 +179,9 @@ public SqlQueryStructure( value: predicate.Value); } - // context.OrderByClauseOfBackingColumns will lack TableAlias because it is created in RequestParser + // context.OrderByClauseOfBackingColumns will lack SourceAlias because it is created in RequestParser // which may be called for any type of operation. To avoid coupling the OrderByClauseOfBackingColumns - // to only Find, we populate the TableAlias in this constructor where we know we have a Find operation. + // to only Find, we populate the SourceAlias in this constructor where we know we have a Find operation. OrderByColumns = context.OrderByClauseOfBackingColumns is not null ? context.OrderByClauseOfBackingColumns : PrimaryKeyAsOrderByColumns(); @@ -185,7 +189,7 @@ public SqlQueryStructure( { if (string.IsNullOrEmpty(column.TableAlias)) { - column.TableAlias = TableAlias; + column.TableAlias = SourceAlias; } } @@ -273,7 +277,7 @@ private List PrimaryKeyAsOrderByColumns() _primaryKeyAsOrderByColumns.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: column, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } } @@ -294,8 +298,9 @@ private SqlQueryStructure( FieldNode? queryField, IncrementingInteger counter, RuntimeConfigProvider runtimeConfigProvider, + GQLFilterParser gQLFilterParser, string entityName = "" - ) : this(sqlMetadataProvider, counter, entityName: entityName) + ) : this(sqlMetadataProvider, counter, gQLFilterParser, entityName: entityName) { AuthorizationResolver = authorizationResolver; _ctx = ctx; @@ -346,7 +351,7 @@ private SqlQueryStructure( DatabaseObject.SchemaName = sqlMetadataProvider.GetSchemaName(EntityName); DatabaseObject.Name = sqlMetadataProvider.GetDatabaseObjectName(EntityName); - TableAlias = CreateTableAlias(); + SourceAlias = CreateTableAlias(); // SelectionSet will not be null when a field is not a leaf. // There may be another entity to resolve as a sub-query. @@ -406,14 +411,11 @@ private SqlQueryStructure( if (filterObject != null) { List filterFields = (List)filterObject; - Predicates.Add(GQLFilterParser.Parse(_ctx, - filterArgumentSchema: queryArgumentSchemas[QueryBuilder.FILTER_FIELD_NAME], - fields: filterFields, - schemaName: DatabaseObject.SchemaName, - sourceName: DatabaseObject.Name, - sourceAlias: TableAlias, - sourceDefinition: GetUnderlyingSourceDefinition(), - processLiterals: MakeParamWithValue)); + Predicates.Add(GraphQLFilterParser.Parse( + _ctx, + filterArgumentSchema: queryArgumentSchemas[QueryBuilder.FILTER_FIELD_NAME], + fields: filterFields, + queryStructure: this)); } } @@ -429,7 +431,7 @@ private SqlQueryStructure( } // need to run after the rest of the query has been processed since it relies on - // TableName, TableAlias, Columns, and _limit + // TableName, SourceAlias, Columns, and _limit if (PaginationMetadata.IsPaginated) { AddPaginationPredicate(SqlPaginationUtil.ParseAfterFromQueryParams(queryParams, PaginationMetadata, sqlMetadataProvider, EntityName, runtimeConfigProvider)); @@ -468,8 +470,9 @@ private SqlQueryStructure( private SqlQueryStructure( ISqlMetadataProvider sqlMetadataProvider, IncrementingInteger counter, + GQLFilterParser gQLFilterParser, string entityName = "") - : base(sqlMetadataProvider, entityName: entityName, counter: counter) + : base(sqlMetadataProvider, gQLFilterParser, entityName: entityName, counter: counter) { JoinQueries = new(); Joins = new(); @@ -490,7 +493,7 @@ private void AddPrimaryKeyPredicates(IDictionary queryParams) new PredicateOperand(new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: parameter.Key, - tableAlias: TableAlias)), + tableAlias: SourceAlias)), PredicateOperation.Equal, new PredicateOperand($"@{MakeParamWithValue(parameter.Value)}") )); @@ -512,7 +515,7 @@ public void AddPaginationPredicate(IEnumerable afterJsonValues { foreach (PaginationColumn column in afterJsonValues) { - column.TableAlias = TableAlias; + column.TableAlias = SourceAlias; column.ParamName = column.Value is not null ? "@" + MakeParamWithValue(GetParamAsColumnSystemType(column.Value!.ToString()!, column.ColumnName)) : "@" + MakeParamWithValue(null); @@ -548,7 +551,7 @@ private void PopulateParamsAndPredicates(string field, string backingColumn, obj parameterName = MakeParamWithValue( GetParamAsColumnSystemType(value.ToString()!, backingColumn)); Predicates.Add(new Predicate( - new PredicateOperand(new Column(DatabaseObject.SchemaName, DatabaseObject.Name, backingColumn, TableAlias)), + new PredicateOperand(new Column(DatabaseObject.SchemaName, DatabaseObject.Name, backingColumn, SourceAlias)), op, new PredicateOperand($"@{parameterName}"))); } @@ -597,14 +600,6 @@ public IEnumerable CreateJoinPredicates( ); } - /// - /// Creates a unique table alias. - /// - public string CreateTableAlias() - { - return $"table{Counter.Next()}"; - } - /// /// Store the requested pagination connection fields and return the fields of the "items" field /// @@ -667,7 +662,7 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC } IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field, _ctx.Variables); - SqlQueryStructure subquery = new(_ctx, subqueryParams, SqlMetadataProvider, AuthorizationResolver, subschemaField, field, Counter, runtimeConfigProvider); + SqlQueryStructure subquery = new(_ctx, subqueryParams, MetadataProvider, AuthorizationResolver, subschemaField, field, Counter, runtimeConfigProvider); if (PaginationMetadata.IsPaginated) { @@ -690,7 +685,7 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC // use the _underlyingType from the subquery which will be overridden appropriately if the query is paginated ObjectType subunderlyingType = subquery._underlyingFieldType; string targetEntityName = subunderlyingType.Name; - string subtableAlias = subquery.TableAlias; + string subtableAlias = subquery.SourceAlias; AddJoinPredicatesForSubQuery(targetEntityName, subtableAlias, subquery); @@ -726,7 +721,7 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC /// created for the given target entity Name and sub table alias. /// There are only a couple of options for the foreign key - we only use the /// valid foreign key definition. It is guaranteed at least one fk definition - /// will be valid since the SqlMetadataProvider.ValidateAllFkHaveBeenInferred. + /// will be valid since the MetadataProvider.ValidateAllFkHaveBeenInferred. /// /// /// @@ -760,7 +755,7 @@ private void AddJoinPredicatesForSubQuery( && foreignKeyDefinition.ReferencedColumns.Count() > 0) { subQuery.Predicates.AddRange(CreateJoinPredicates( - TableAlias, + SourceAlias, foreignKeyDefinition.ReferencingColumns, subtableAlias, foreignKeyDefinition.ReferencedColumns)); @@ -775,7 +770,7 @@ private void AddJoinPredicatesForSubQuery( subQuery.Predicates.AddRange(CreateJoinPredicates( subtableAlias, foreignKeyDefinition.ReferencingColumns, - TableAlias, + SourceAlias, foreignKeyDefinition.ReferencedColumns)); } } @@ -799,7 +794,7 @@ private void AddJoinPredicatesForSubQuery( subQuery.Predicates.AddRange(CreateJoinPredicates( associativeTableAlias, foreignKeyDefinition.ReferencingColumns, - TableAlias, + SourceAlias, foreignKeyDefinition.ReferencedColumns)); } else @@ -865,7 +860,7 @@ private List ProcessGqlOrderByArg(List orderByFi orderByColumnsList.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: fieldName, - tableAlias: TableAlias, + tableAlias: SourceAlias, direction: OrderBy.DESC)); } else @@ -873,7 +868,7 @@ private List ProcessGqlOrderByArg(List orderByFi orderByColumnsList.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: fieldName, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } } @@ -882,7 +877,7 @@ private List ProcessGqlOrderByArg(List orderByFi orderByColumnsList.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: colName, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } return orderByColumnsList; @@ -905,7 +900,7 @@ protected void AddColumn(string columnName) /// protected void AddColumn(string columnName, string labelName) { - Columns.Add(new LabelledColumn(DatabaseObject.SchemaName, DatabaseObject.Name, columnName, label: labelName, TableAlias)); + Columns.Add(new LabelledColumn(DatabaseObject.SchemaName, DatabaseObject.Name, columnName, label: labelName, SourceAlias)); } /// diff --git a/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs index 33d684c190..2442f36d4d 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs @@ -42,7 +42,7 @@ public SqlUpdateStructure( { Predicate predicate = CreatePredicateForParam(param); // since we have already validated mutationParams we know backing column exists - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); // primary keys used as predicates if (primaryKeys.Contains(backingColumn!)) { @@ -123,7 +123,7 @@ private Predicate CreatePredicateForParam(KeyValuePair param) SourceDefinition sourceDefinition = GetUnderlyingSourceDefinition(); Predicate predicate; // since we have already validated param we know backing column exists - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); if (param.Value is null && !sourceDefinition.Columns[backingColumn!].IsNullable) { throw new DataApiBuilderException( diff --git a/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs index c7f145a6ab..d4137db564 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs @@ -104,7 +104,7 @@ private void PopulateColumns( foreach (KeyValuePair param in mutationParams) { // since we have already validated mutationParams we know backing column exists - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); // Create Parameter and map it to column for downstream logic to utilize. string paramIdentifier; if (param.Value != null) diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 748240a674..45c889b875 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -30,6 +30,7 @@ public class SqlQueryEngine : IQueryEngine private readonly IAuthorizationResolver _authorizationResolver; private readonly ILogger _logger; private readonly RuntimeConfigProvider _runtimeConfigProvider; + private readonly GQLFilterParser _gQLFilterParser; // // Constructor. @@ -41,6 +42,7 @@ public SqlQueryEngine( IHttpContextAccessor httpContextAccessor, IAuthorizationResolver authorizationResolver, ILogger logger, + GQLFilterParser gQLFilterParser, RuntimeConfigProvider runtimeConfigProvider) { _queryExecutor = queryExecutor; @@ -48,6 +50,7 @@ public SqlQueryEngine( _sqlMetadataProvider = sqlMetadataProvider; _httpContextAccessor = httpContextAccessor; _authorizationResolver = authorizationResolver; + _gQLFilterParser = gQLFilterParser; _logger = logger; _runtimeConfigProvider = runtimeConfigProvider; } @@ -61,7 +64,13 @@ public SqlQueryEngine( /// GraphQL Query Parameters from schema retrieved from ResolverMiddleware.GetParametersFromSchemaAndQueryFields() public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { - SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); + SqlQueryStructure structure = new( + context, + parameters, + _sqlMetadataProvider, + _authorizationResolver, + _runtimeConfigProvider, + _gQLFilterParser); if (structure.PaginationMetadata.IsPaginated) { @@ -83,7 +92,13 @@ await ExecuteAsync(structure), /// public async Task, IMetadata>> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) { - SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); + SqlQueryStructure structure = new( + context, + parameters, + _sqlMetadataProvider, + _authorizationResolver, + _runtimeConfigProvider, + _gQLFilterParser); string queryString = _queryBuilder.Build(structure); _logger.LogInformation(queryString); List jsonListResult = @@ -107,7 +122,11 @@ await _queryExecutor.ExecuteQueryAsync( // public async Task ExecuteAsync(FindRequestContext context) { - SqlQueryStructure structure = new(context, _sqlMetadataProvider, _runtimeConfigProvider); + SqlQueryStructure structure = new( + context, + _sqlMetadataProvider, + _runtimeConfigProvider, + _gQLFilterParser); using JsonDocument queryJson = await ExecuteAsync(structure); // queryJson is null if dbreader had no rows to return // If no rows/empty table, return an empty json array diff --git a/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 2cb14d19e0..5c616ee864 100644 --- a/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -96,9 +96,15 @@ string db when string.IsNullOrEmpty(db) && !string.IsNullOrEmpty(_cosmosDb.Datab }; } + /// + /// Even though there is no source definition for underlying entity names for + /// cosmos db, we return back an empty source definition required for + /// graphql filter parser. + /// + /// public SourceDefinition GetSourceDefinition(string entityName) { - throw new NotSupportedException("Cosmos backends don't support direct table definitions. Definitions are provided via the GraphQL schema"); + return new SourceDefinition(); } public StoredProcedureDefinition GetStoredProcedureDefinition(string entityName) diff --git a/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs index 09426fb301..a454a1028b 100644 --- a/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; @@ -118,5 +120,27 @@ bool VerifyForeignKeyExistsInDB( /// If no match found, then use the GraphQL singular type in the runtime config to look up the top-level entity name from a GraphQLSingularTypeToEntityNameMap /// public string GetEntityName(string graphQLType); + + /// + /// For the given graphql type, returns the inferred database object. + /// Does this by first looking up the entity name from the graphql type to entity name mapping. + /// Subsequently, looks up the database object inferred for the corresponding entity. + /// + /// Name of the graphql type + /// Underlying inferred DatabaseObject. + /// Thrown if entity is not found. + public DatabaseObject GetDatabaseObjectForGraphQLType(string graphqlType) + { + string entityName = GetEntityName(graphqlType); + + if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject)) + { + throw new DataApiBuilderException(message: $"Source Definition for {entityName} has not been inferred.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); + } + + return databaseObject; + } } } diff --git a/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs index 7360c0a9f5..663bdb91aa 100644 --- a/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -9,7 +9,7 @@ namespace Azure.DataApiBuilder.Service.Services { /// - /// MsSQL specific override for SqlMetadataProvider. + /// MsSQL specific override for MetadataProvider. /// All the method definitions from base class are sufficient /// this class is only created for symmetricity with MySql /// and ease of expanding the generics specific to MsSql. diff --git a/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs index 9fb172141a..6838674266 100644 --- a/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs @@ -12,7 +12,7 @@ namespace Azure.DataApiBuilder.Service.Services { /// - /// MySQL specific override for SqlMetadataProvider + /// MySQL specific override for MetadataProvider /// public class MySqlMetadataProvider : SqlMetadataProvider, ISqlMetadataProvider { diff --git a/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs index 8734532b3f..7d9ad3098d 100644 --- a/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs @@ -9,7 +9,7 @@ namespace Azure.DataApiBuilder.Service.Services { /// - /// PostgreSql specific override for SqlMetadataProvider. + /// PostgreSql specific override for MetadataProvider. /// All the method definitions from base class are sufficient /// this class is only created for symmetricity with MySql /// and ease of expanding the generics specific to PostgreSql. diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 17cb34035b..1611a59df2 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -32,6 +32,9 @@ public abstract class SqlMetadataProvider : private readonly Dictionary _entities; + // Dictionary mapping singular graphql types to entity name keys in the configuration + private readonly Dictionary _graphQLSingularTypeToEntityNameMap = new(); + // Contains all the referencing and referenced columns for each pair // of referencing and referenced tables. private Dictionary? _pairToFkDefinition; @@ -72,6 +75,7 @@ public SqlMetadataProvider( _runtimeConfigProvider = runtimeConfigProvider; _databaseType = runtimeConfig.DatabaseType; _entities = runtimeConfig.Entities; + _graphQLSingularTypeToEntityNameMap = runtimeConfig.GraphQLSingularTypeToEntityNameMap; _logger = logger; foreach (KeyValuePair entity in _entities) { @@ -198,6 +202,25 @@ public IDictionary GetEntityNamesAndDbObjects() return EntityToDatabaseObject; } + /// + public string GetEntityName(string graphQLType) + { + if (_entities.ContainsKey(graphQLType)) + { + return graphQLType; + } + + if (_graphQLSingularTypeToEntityNameMap.TryGetValue(graphQLType, out string? entityName)) + { + throw new DataApiBuilderException( + "GraphQL type doesn't match any entity name or singular type in the runtime config.", + System.Net.HttpStatusCode.BadRequest, + DataApiBuilderException.SubStatusCodes.BadRequest); + } + + return entityName!; + } + /// public async Task InitializeAsync() { @@ -1309,10 +1332,6 @@ public bool VerifyForeignKeyExistsInDB( /// public void SetPartitionKeyPath(string database, string container, string partitionKeyPath) => throw new NotImplementedException(); - - /// - public string GetEntityName(string graphQLType) - => throw new NotImplementedException(); } } From 03c74aa13dfddbb5e29d88e7402e2e630f71b7b7 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 15:12:07 -0800 Subject: [PATCH 09/31] Fix build failures --- .../GraphQLMutationAuthorizationTests.cs | 3 ++ src/Service.Tests/SqlTests/SqlTestBase.cs | 7 ++++- .../Unittests/RestServiceUnitTests.cs | 5 +++- src/Service/Resolvers/CosmosQueryEngine.cs | 4 ++- .../SqlDeleteQueryStructure.cs | 5 +++- .../SqlExecuteQueryStructure.cs | 5 +++- .../SqlInsertQueryStructure.cs | 9 +++++- .../SqlUpdateQueryStructure.cs | 9 ++++-- .../SqlUpsertQueryStructure.cs | 5 +++- src/Service/Resolvers/SqlMutationEngine.cs | 30 +++++++++++++++++-- src/Service/Resolvers/SqlQueryEngine.cs | 9 ++++-- 11 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index 654b051f7f..e52892e70d 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -6,6 +6,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; @@ -107,6 +108,7 @@ private static SqlMutationEngine SetupTestFixture(bool isAuthorized) Mock httpContextAccessor = new(); Mock> _mutationEngineLogger = new(); DefaultHttpContext context = new(); + Mock _gQLFilterParser = new(); httpContextAccessor.Setup(_ => _.HttpContext).Returns(context); // Creates Mock AuthorizationResolver to return a preset result based on [TestMethod] input. @@ -124,6 +126,7 @@ private static SqlMutationEngine SetupTestFixture(bool isAuthorized) _queryBuilder.Object, _sqlMetadataProvider.Object, _authorizationResolver.Object, + _gQLFilterParser.Object, httpContextAccessor.Object, _mutationEngineLogger.Object ); diff --git a/src/Service.Tests/SqlTests/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs index 68bc4af994..5be14f2fd3 100644 --- a/src/Service.Tests/SqlTests/SqlTestBase.cs +++ b/src/Service.Tests/SqlTests/SqlTestBase.cs @@ -14,6 +14,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Controllers; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using Microsoft.AspNetCore.Authorization; @@ -53,6 +54,7 @@ public abstract class SqlTestBase protected static ILogger _mutationEngineLogger; protected static ILogger _queryEngineLogger; protected static ILogger _restControllerLogger; + protected static GQLFilterParser _gQLFilterParser; protected const string MSSQL_DEFAULT_DB_NAME = "master"; protected static string DatabaseName { get; set; } @@ -102,7 +104,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(); _httpContextAccessor.Setup(x => x.HttpContext.User).Returns(new ClaimsPrincipal()); - + _gQLFilterParser = new(_sqlMetadataProvider); await ResetDbStateAsync(); // Execute additional queries, if any. @@ -126,6 +128,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(implementationFactory: (serviceProvider) => { return new SqlQueryEngine( @@ -134,6 +137,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(serviceProvider), _authorizationResolver, + _gQLFilterParser, _queryEngineLogger, _runtimeConfigProvider ); @@ -146,6 +150,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(serviceProvider), _mutationEngineLogger); }); diff --git a/src/Service.Tests/Unittests/RestServiceUnitTests.cs b/src/Service.Tests/Unittests/RestServiceUnitTests.cs index b88a3899f4..9c3582dda3 100644 --- a/src/Service.Tests/Unittests/RestServiceUnitTests.cs +++ b/src/Service.Tests/Unittests/RestServiceUnitTests.cs @@ -4,6 +4,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using Microsoft.AspNetCore.Authorization; @@ -147,13 +148,14 @@ public static void InitializeTest(string path, string entityName) DefaultHttpContext context = new(); httpContextAccessor.Setup(_ => _.HttpContext).Returns(context); AuthorizationResolver authorizationResolver = new(runtimeConfigProvider, sqlMetadataProvider.Object, authLogger.Object); - + GQLFilterParser gQLFilterParser = new(sqlMetadataProvider.Object); SqlQueryEngine queryEngine = new( queryExecutor, queryBuilder, sqlMetadataProvider.Object, httpContextAccessor.Object, authorizationResolver, + gQLFilterParser, queryEngineLogger.Object, runtimeConfigProvider); @@ -164,6 +166,7 @@ public static void InitializeTest(string path, string entityName) queryBuilder, sqlMetadataProvider.Object, authorizationResolver, + gQLFilterParser, httpContextAccessor.Object, mutationEngingLogger.Object); diff --git a/src/Service/Resolvers/CosmosQueryEngine.cs b/src/Service/Resolvers/CosmosQueryEngine.cs index e8d770817b..6afb13f244 100644 --- a/src/Service/Resolvers/CosmosQueryEngine.cs +++ b/src/Service/Resolvers/CosmosQueryEngine.cs @@ -45,7 +45,9 @@ public CosmosQueryEngine( /// Executes the given IMiddlewareContext of the GraphQL query and /// expecting a single Json back. /// - public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) + public async Task> ExecuteAsync( + IMiddlewareContext context, + IDictionary parameters) { // TODO: fixme we have multiple rounds of serialization/deserialization JsomDocument/JObject // TODO: add support for nesting diff --git a/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs index 9d14477336..84eb69e5f0 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; @@ -15,8 +16,10 @@ public class SqlDeleteStructure : BaseSqlQueryStructure public SqlDeleteStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { SourceDefinition sourceDefinition = GetUnderlyingSourceDefinition(); diff --git a/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs index 8633a8390e..33732cf449 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Services; @@ -26,8 +27,10 @@ public class SqlExecuteStructure : BaseSqlQueryStructure public SqlExecuteStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary requestParams) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { StoredProcedureDefinition storedProcedureDefinition = GetUnderlyingStoredProcedureDefinition(); ProcedureParameters = new(); diff --git a/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs index 4f4ec9cf2a..a4863beb99 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; @@ -33,19 +34,25 @@ public SqlInsertStructure( IMiddlewareContext context, string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams ) : this( entityName, sqlMetadataProvider, + authorizationResolver, + gQLFilterParser, GQLMutArgumentToDictParams(context, CreateMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams)) { } public SqlInsertStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams ) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { InsertColumns = new(); Values = new(); diff --git a/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs index 2442f36d4d..8ace3c70a5 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; @@ -28,9 +29,11 @@ public class SqlUpdateStructure : BaseSqlQueryStructure public SqlUpdateStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams, bool isIncrementalUpdate) - : base(sqlMetadataProvider, entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { UpdateOperations = new(); OutputColumns = GenerateOutputColumns(); @@ -79,8 +82,10 @@ public SqlUpdateStructure( IMiddlewareContext context, string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { UpdateOperations = new(); SourceDefinition sourceDefinition = GetUnderlyingSourceDefinition(); diff --git a/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs index d4137db564..9eab30c1db 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; @@ -57,9 +58,11 @@ public class SqlUpsertQueryStructure : BaseSqlQueryStructure public SqlUpsertQueryStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams, bool incrementalUpdate) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { UpdateOperations = new(); InsertColumns = new(); diff --git a/src/Service/Resolvers/SqlMutationEngine.cs b/src/Service/Resolvers/SqlMutationEngine.cs index 8a1fa1e2f0..6cfa4b0e79 100644 --- a/src/Service/Resolvers/SqlMutationEngine.cs +++ b/src/Service/Resolvers/SqlMutationEngine.cs @@ -36,6 +36,7 @@ public class SqlMutationEngine : IMutationEngine private readonly IAuthorizationResolver _authorizationResolver; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; + private readonly GQLFilterParser _gQLFilterParser; public const string IS_FIRST_RESULT_SET = "IsFirstResultSet"; /// @@ -47,6 +48,7 @@ public SqlMutationEngine( IQueryBuilder queryBuilder, ISqlMetadataProvider sqlMetadataProvider, IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IHttpContextAccessor httpContextAccessor, ILogger logger) { @@ -57,6 +59,7 @@ public SqlMutationEngine( _authorizationResolver = authorizationResolver; _httpContextAccessor = httpContextAccessor; _logger = logger; + _gQLFilterParser = gQLFilterParser; } /// @@ -150,7 +153,12 @@ await PerformMutationOperation( /// public async Task ExecuteAsync(StoredProcedureRequestContext context) { - SqlExecuteStructure executeQueryStructure = new(context.EntityName, _sqlMetadataProvider, context.ResolvedParameters!); + SqlExecuteStructure executeQueryStructure = new( + context.EntityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + context.ResolvedParameters); string queryText = _queryBuilder.Build(executeQueryStructure); _logger.LogInformation(queryText); @@ -367,8 +375,14 @@ private static OkObjectResult OkMutationResponse(Dictionary? re SqlInsertStructure insertQueryStruct = context is null ? new(entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters) : - new(context, entityName, _sqlMetadataProvider, parameters); + new(context, + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters); queryString = _queryBuilder.Build(insertQueryStruct); queryParameters = insertQueryStruct.Parameters; break; @@ -376,6 +390,8 @@ private static OkObjectResult OkMutationResponse(Dictionary? re SqlUpdateStructure updateStructure = new(entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters, isIncrementalUpdate: false); queryString = _queryBuilder.Build(updateStructure); @@ -385,6 +401,8 @@ private static OkObjectResult OkMutationResponse(Dictionary? re SqlUpdateStructure updateIncrementalStructure = new(entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters, isIncrementalUpdate: true); queryString = _queryBuilder.Build(updateIncrementalStructure); @@ -401,6 +419,8 @@ private static OkObjectResult OkMutationResponse(Dictionary? re context, entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters); AuthorizationPolicyHelpers.ProcessAuthorizationPolicies( Operation.Update, @@ -476,6 +496,8 @@ private async Task?> SqlDeleteStructure deleteStructure = new(entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters); AuthorizationPolicyHelpers.ProcessAuthorizationPolicies( Operation.Delete, @@ -521,6 +543,8 @@ private async Task?> SqlUpsertQueryStructure upsertStructure = new(entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters, incrementalUpdate: false); queryString = _queryBuilder.Build(upsertStructure); @@ -531,6 +555,8 @@ private async Task?> SqlUpsertQueryStructure upsertIncrementalStructure = new(entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters, incrementalUpdate: true); queryString = _queryBuilder.Build(upsertIncrementalStructure); diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 45c889b875..ac266032b3 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -41,8 +41,8 @@ public SqlQueryEngine( ISqlMetadataProvider sqlMetadataProvider, IHttpContextAccessor httpContextAccessor, IAuthorizationResolver authorizationResolver, - ILogger logger, GQLFilterParser gQLFilterParser, + ILogger logger, RuntimeConfigProvider runtimeConfigProvider) { _queryExecutor = queryExecutor; @@ -140,7 +140,12 @@ public async Task ExecuteAsync(FindRequestContext context) /// public async Task ExecuteAsync(StoredProcedureRequestContext context) { - SqlExecuteStructure structure = new(context.EntityName, _sqlMetadataProvider, context.ResolvedParameters); + SqlExecuteStructure structure = new( + context.EntityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + context.ResolvedParameters); using JsonDocument queryJson = await ExecuteAsync(structure); // queryJson is null if dbreader had no rows to return // If no rows/empty result set, return an empty json array From 68114a50916b5a252c805f075444a63393f5d9b2 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 17:17:41 -0800 Subject: [PATCH 10/31] Fix further build failures --- src/Service/Resolvers/BaseQueryStructure.cs | 12 ++++++++++++ .../Sql Query Structures/BaseSqlQueryStructure.cs | 10 +++++++--- .../Sql Query Structures/SqlQueryStructure.cs | 6 ------ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Service/Resolvers/BaseQueryStructure.cs b/src/Service/Resolvers/BaseQueryStructure.cs index 17a5e969d0..f8670fa9e0 100644 --- a/src/Service/Resolvers/BaseQueryStructure.cs +++ b/src/Service/Resolvers/BaseQueryStructure.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; @@ -51,10 +52,20 @@ public class BaseQueryStructure /// public List Predicates { get; } + /// + /// Used for parsing graphql filter arguments. + /// public GQLFilterParser GraphQLFilterParser { get; protected set; } + /// + /// Authorization Resolver used within SqlQueryStructure to get and apply + /// authorization policies to requests. + /// + protected IAuthorizationResolver AuthorizationResolver { get; } + public BaseQueryStructure( ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser, string entityName, IncrementingInteger? counter = null) @@ -65,6 +76,7 @@ public BaseQueryStructure( Counter = counter ?? new IncrementingInteger(); MetadataProvider = metadataProvider; GraphQLFilterParser = gQLFilterParser; + AuthorizationResolver = authorizationResolver; // Default the alias to the empty string since this base construtor // is called for requests other than Find operations. We only use diff --git a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 3b2426cef3..5dc3f1ea58 100644 --- a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -3,7 +3,9 @@ using System.IO; using System.Linq; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Parsers; @@ -36,11 +38,13 @@ public abstract class BaseSqlQueryStructure : BaseQueryStructure public string? DbPolicyPredicates { get; set; } public BaseSqlQueryStructure( - ISqlMetadataProvider sqlMetadataProvider, + ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser, - string entityName, + List? predicates = null, + string entityName = "", IncrementingInteger? counter = null) - : base(sqlMetadataProvider, gQLFilterParser, entityName, counter) + : base(metadataProvider, authorizationResolver, gQLFilterParser, predicates, entityName, counter) { } diff --git a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 7a4d5e78af..f0cc00738f 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -28,12 +28,6 @@ namespace Azure.DataApiBuilder.Service.Resolvers /// public class SqlQueryStructure : BaseSqlQueryStructure { - /// - /// Authorization Resolver used within SqlQueryStructure to get and apply - /// authorization policies to requests. - /// - protected IAuthorizationResolver AuthorizationResolver { get; } - public const string DATA_IDENT = "data"; /// From a46c6dd85f24c81cc1500a9be747e1551189e14c Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 17:28:44 -0800 Subject: [PATCH 11/31] Fix remaining build failures --- src/Service/Resolvers/BaseQueryStructure.cs | 5 ++- src/Service/Resolvers/CosmosQueryEngine.cs | 7 ++- src/Service/Resolvers/CosmosQueryStructure.cs | 4 +- .../BaseSqlQueryStructure.cs | 1 - .../SqlExecuteQueryStructure.cs | 1 + .../Sql Query Structures/SqlQueryStructure.cs | 44 +++++++++++++------ src/Service/Resolvers/SqlQueryEngine.cs | 1 + 7 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/Service/Resolvers/BaseQueryStructure.cs b/src/Service/Resolvers/BaseQueryStructure.cs index f8670fa9e0..8a10fc8661 100644 --- a/src/Service/Resolvers/BaseQueryStructure.cs +++ b/src/Service/Resolvers/BaseQueryStructure.cs @@ -67,11 +67,12 @@ public BaseQueryStructure( ISqlMetadataProvider metadataProvider, IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser, - string entityName, + List? predicates = null, + string entityName = "", IncrementingInteger? counter = null) { Columns = new(); - Predicates = new(); + Predicates = predicates ?? new(); Parameters = new(); Counter = counter ?? new IncrementingInteger(); MetadataProvider = metadataProvider; diff --git a/src/Service/Resolvers/CosmosQueryEngine.cs b/src/Service/Resolvers/CosmosQueryEngine.cs index 6afb13f244..d190bd9b62 100644 --- a/src/Service/Resolvers/CosmosQueryEngine.cs +++ b/src/Service/Resolvers/CosmosQueryEngine.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Services; @@ -26,6 +27,7 @@ public class CosmosQueryEngine : IQueryEngine private readonly ISqlMetadataProvider _metadataStoreProvider; private readonly CosmosQueryBuilder _queryBuilder; private readonly GQLFilterParser _gQLFilterParser; + private readonly IAuthorizationResolver _authorizationResolver; // // Constructor. @@ -33,6 +35,7 @@ public class CosmosQueryEngine : IQueryEngine public CosmosQueryEngine( CosmosClientProvider clientProvider, ISqlMetadataProvider metadataStoreProvider, + IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser) { _clientProvider = clientProvider; @@ -54,7 +57,7 @@ public async Task> ExecuteAsync( // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down - CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider, _gQLFilterParser); + CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider, _authorizationResolver, _gQLFilterParser); string requestContinuation = null; string queryString = _queryBuilder.Build(structure); @@ -138,7 +141,7 @@ public async Task, IMetadata>> ExecuteListAsync( // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down - CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider); + CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider, _authorizationResolver, _gQLFilterParser); Container container = _clientProvider.Client.GetDatabase(structure.Database).GetContainer(structure.Container); QueryDefinition querySpec = new(_queryBuilder.Build(structure)); diff --git a/src/Service/Resolvers/CosmosQueryStructure.cs b/src/Service/Resolvers/CosmosQueryStructure.cs index 20f2027ff8..6498d2cf7d 100644 --- a/src/Service/Resolvers/CosmosQueryStructure.cs +++ b/src/Service/Resolvers/CosmosQueryStructure.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes; @@ -33,8 +34,9 @@ public CosmosQueryStructure( IMiddlewareContext context, IDictionary parameters, ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser) - : base(metadataProvider, gQLFilterParser, entityName: string.Empty) + : base(metadataProvider, authorizationResolver, gQLFilterParser, entityName: string.Empty) { _context = context; SourceAlias = _containerAlias; diff --git a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 5dc3f1ea58..6b788339de 100644 --- a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -5,7 +5,6 @@ using System.Net; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Parsers; diff --git a/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs index 33732cf449..ecd6485e2a 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs @@ -4,6 +4,7 @@ using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Services; namespace Azure.DataApiBuilder.Service.Resolvers diff --git a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index f0cc00738f..ba6a4d7144 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -134,12 +134,15 @@ public SqlQueryStructure( public SqlQueryStructure( RestRequestContext context, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, RuntimeConfigProvider runtimeConfigProvider, - GQLFilterParser gQLFilterParser) : - this(sqlMetadataProvider, - new IncrementingInteger(), - gQLFilterParser, - entityName: context.EntityName) + GQLFilterParser gQLFilterParser) + : this(sqlMetadataProvider, + authorizationResolver, + gQLFilterParser, + predicates: null, + entityName: context.EntityName, + counter: new IncrementingInteger()) { IsListQuery = context.IsMany; SourceAlias = $"{DatabaseObject.SchemaName}_{DatabaseObject.Name}"; @@ -293,10 +296,14 @@ private SqlQueryStructure( IncrementingInteger counter, RuntimeConfigProvider runtimeConfigProvider, GQLFilterParser gQLFilterParser, - string entityName = "" - ) : this(sqlMetadataProvider, counter, gQLFilterParser, entityName: entityName) + string entityName = "") + : this (sqlMetadataProvider, + authorizationResolver, + gQLFilterParser, + predicates: null, + entityName: entityName, + counter) { - AuthorizationResolver = authorizationResolver; _ctx = ctx; IOutputType outputType = schemaField.Type; _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); @@ -462,11 +469,13 @@ private SqlQueryStructure( /// constructors. /// private SqlQueryStructure( - ISqlMetadataProvider sqlMetadataProvider, - IncrementingInteger counter, + ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser, - string entityName = "") - : base(sqlMetadataProvider, gQLFilterParser, entityName: entityName, counter: counter) + List? predicates = null, + string entityName = "", + IncrementingInteger? counter = null) + : base(metadataProvider, authorizationResolver, gQLFilterParser, predicates, entityName, counter) { JoinQueries = new(); Joins = new(); @@ -656,7 +665,16 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC } IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field, _ctx.Variables); - SqlQueryStructure subquery = new(_ctx, subqueryParams, MetadataProvider, AuthorizationResolver, subschemaField, field, Counter, runtimeConfigProvider); + SqlQueryStructure subquery = new( + _ctx, + subqueryParams, + MetadataProvider, + AuthorizationResolver, + subschemaField, + field, + Counter, + runtimeConfigProvider, + GraphQLFilterParser); if (PaginationMetadata.IsPaginated) { diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index ac266032b3..61d9286296 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -125,6 +125,7 @@ public async Task ExecuteAsync(FindRequestContext context) SqlQueryStructure structure = new( context, _sqlMetadataProvider, + _authorizationResolver, _runtimeConfigProvider, _gQLFilterParser); using JsonDocument queryJson = await ExecuteAsync(structure); From 493d5c3bf1729852dfd6686b7f13057963052a00 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 17:29:20 -0800 Subject: [PATCH 12/31] Fix formatting --- src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index ba6a4d7144..fa233e42b0 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -297,7 +297,7 @@ private SqlQueryStructure( RuntimeConfigProvider runtimeConfigProvider, GQLFilterParser gQLFilterParser, string entityName = "") - : this (sqlMetadataProvider, + : this(sqlMetadataProvider, authorizationResolver, gQLFilterParser, predicates: null, From 1ec3932533dca3f4ca2bf2568a98beb62e089abf Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 17:31:12 -0800 Subject: [PATCH 13/31] Revert "Add a relationship directive on input field node" This reverts commit 71f7c3d692b8da193949ec7456ecf68d65a10f7e. --- .../Directives/RelationshipDirective.cs | 37 ++++--------------- .../Queries/InputTypeBuilder.cs | 12 ++---- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs b/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs index d3ab4f9153..0788622ca8 100644 --- a/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs +++ b/src/Service.GraphQLBuilder/Directives/RelationshipDirective.cs @@ -13,7 +13,7 @@ protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor.Name(DirectiveName) .Description("A directive to indicate the relationship between two tables") - .Location(DirectiveLocation.FieldDefinition | DirectiveLocation.InputFieldDefinition); + .Location(DirectiveLocation.FieldDefinition); descriptor.Argument("target") .Type() @@ -25,10 +25,10 @@ protected override void Configure(IDirectiveTypeDescriptor descriptor) } /// - /// Gets the target object type name for a ifield with a relationship directive. + /// Gets the target object type name for a field with a relationship directive. /// - /// The ifield that has a relationship directive defined. - /// The name of the GraphQL object type that the relationship targets. If no relationship is defined, the object type of the ifield is returned. + /// The field that has a relationship directive defined. + /// The name of the GraphQL object type that the relationship targets. If no relationship is defined, the object type of the field is returned. public static string Target(FieldDefinitionNode field) { DirectiveNode? directive = field.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); @@ -43,47 +43,24 @@ public static string Target(FieldDefinitionNode field) return (string)arg.Value.Value!; } - /// - /// Gets the target object type name for an input ifield with a relationship directive. - /// - /// The input field that is expected to have a relationship directive defined on it. - /// The name of the target object if the relationship is found, null otherwise. - public static string? Target(InputField ifield) - { - Directive? directive = (Directive?)ifield.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); - DirectiveNode? directiveNode = directive?.ToNode(); - ArgumentNode? arg = directiveNode?.Arguments.First(a => a.Name.Value == "target"); - - return (string?)arg?.Value.Value; - } - /// /// Gets the cardinality of the relationship. /// - /// The ifield that has a relationship directive defined. + /// The field that has a relationship directive defined. /// Relationship cardinality - /// Thrown if the ifield does not have a defined relationship. + /// Thrown if the field does not have a defined relationship. public static Cardinality Cardinality(FieldDefinitionNode field) { DirectiveNode? directive = field.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); if (directive == null) { - throw new ArgumentException("The specified ifield does not have a relationship directive defined."); + throw new ArgumentException("The specified field does not have a relationship directive defined."); } ArgumentNode arg = directive.Arguments.First(a => a.Name.Value == "cardinality"); return Enum.Parse((string)arg.Value.Value!); } - - /// - /// Retrieve the relationship directive defined on the given ifield definition node. - /// - public static DirectiveNode? GetDirective(FieldDefinitionNode field) - { - DirectiveNode? directive = field.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); - return directive; - } } } diff --git a/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs b/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs index b1c9b25541..4dd9c5a6f8 100644 --- a/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs @@ -137,14 +137,6 @@ private static List GenerateFilterInputFieldsForBuiltI { string targetEntityName = RelationshipDirectiveType.Target(field); - DirectiveNode? relationshipDirective = - RelationshipDirectiveType.GetDirective(field); - List directives = new(); - if (relationshipDirective is not null) - { - directives.Add(relationshipDirective); - } - inputFields.Add( new( location: null, @@ -152,8 +144,10 @@ private static List GenerateFilterInputFieldsForBuiltI new StringValueNode($"Filter options for {field.Name}"), new NamedTypeNode(GenerateObjectInputFilterName(targetEntityName)), defaultValue: null, - directives: directives)); + new List()) + ); } + } return inputFields; From 138918e5eb04e2b8ddafcbc34b2501d0b1a7a78d Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 16:24:25 -0800 Subject: [PATCH 14/31] Add singleton service GQLFilterParser --- src/Service/Startup.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index b5c5b9f913..b084252883 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -10,6 +10,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; @@ -177,6 +178,7 @@ public void ConfigureServices(IServiceCollection services) }); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); From b047cca745535dba0cffeb8cd2b80100e5154776 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 17:40:59 -0800 Subject: [PATCH 15/31] Remove not yet required function GetDatabaseObjectForGraphQLType --- .../MetadataProviders/ISqlMetadataProvider.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs index a454a1028b..93f2950b87 100644 --- a/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -120,27 +120,5 @@ bool VerifyForeignKeyExistsInDB( /// If no match found, then use the GraphQL singular type in the runtime config to look up the top-level entity name from a GraphQLSingularTypeToEntityNameMap /// public string GetEntityName(string graphQLType); - - /// - /// For the given graphql type, returns the inferred database object. - /// Does this by first looking up the entity name from the graphql type to entity name mapping. - /// Subsequently, looks up the database object inferred for the corresponding entity. - /// - /// Name of the graphql type - /// Underlying inferred DatabaseObject. - /// Thrown if entity is not found. - public DatabaseObject GetDatabaseObjectForGraphQLType(string graphqlType) - { - string entityName = GetEntityName(graphqlType); - - if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject)) - { - throw new DataApiBuilderException(message: $"Source Definition for {entityName} has not been inferred.", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); - } - - return databaseObject; - } } } From 08faa7f7d12056a53d4d3b02af56edfebc3dbeeb Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 17:41:49 -0800 Subject: [PATCH 16/31] Fix formatting --- src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs index 93f2950b87..09426fb301 100644 --- a/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Net; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; From b5cb065fd0f3c9626db54185402db69c2c3fd259 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 20:24:12 -0800 Subject: [PATCH 17/31] Fix unnecessary rename --- src/Service/Parsers/RequestParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Parsers/RequestParser.cs b/src/Service/Parsers/RequestParser.cs index ce093c89c5..af1cf9d4e6 100644 --- a/src/Service/Parsers/RequestParser.cs +++ b/src/Service/Parsers/RequestParser.cs @@ -92,7 +92,7 @@ public static void ParsePrimaryKey(string primaryKeyRoute, RestRequestContext co /// so any instance of a null key will result in a bad request. /// /// The RestRequestContext holding the major components of the query. - /// The MetadataProvider holds many of the components needed to parse the query. + /// The SqlMetadataProvider holds many of the components needed to parse the query. /// public static void ParseQueryString(RestRequestContext context, ISqlMetadataProvider sqlMetadataProvider) { From 198affbe48b5ccca21aba67195f18c0ce239328a Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 20:26:04 -0800 Subject: [PATCH 18/31] Fix DatabaseObject.Name for Cosmos --- src/Service/Resolvers/BaseQueryStructure.cs | 14 ++++++++------ src/Service/Resolvers/CosmosQueryStructure.cs | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Service/Resolvers/BaseQueryStructure.cs b/src/Service/Resolvers/BaseQueryStructure.cs index 8a10fc8661..0ad7a1bd96 100644 --- a/src/Service/Resolvers/BaseQueryStructure.cs +++ b/src/Service/Resolvers/BaseQueryStructure.cs @@ -13,15 +13,18 @@ namespace Azure.DataApiBuilder.Service.Resolvers public class BaseQueryStructure { /// - /// The Entity associated with this query. + /// The Entity name associated with this query as appears in the config file. /// public string EntityName { get; protected set; } /// - /// The alias of the main entity to be queried. + /// The alias of the entity as used in the generated query. /// public virtual string SourceAlias { get; set; } + /// + /// The metadata provider of the respective database. + /// protected ISqlMetadataProvider MetadataProvider { get; } /// @@ -58,7 +61,7 @@ public class BaseQueryStructure public GQLFilterParser GraphQLFilterParser { get; protected set; } /// - /// Authorization Resolver used within SqlQueryStructure to get and apply + /// Authorization Resolver used to get and apply /// authorization policies to requests. /// protected IAuthorizationResolver AuthorizationResolver { get; } @@ -93,9 +96,8 @@ public BaseQueryStructure( else { EntityName = string.Empty; - // This is the cosmos db metadata scenario - // where the table name is the Source Alias i.e. container alias - DatabaseObject = new DatabaseTable(string.Empty, SourceAlias); + DatabaseObject = + new DatabaseTable(schemaName: string.Empty, tableName: string.Empty); } } diff --git a/src/Service/Resolvers/CosmosQueryStructure.cs b/src/Service/Resolvers/CosmosQueryStructure.cs index 6498d2cf7d..f11078af2d 100644 --- a/src/Service/Resolvers/CosmosQueryStructure.cs +++ b/src/Service/Resolvers/CosmosQueryStructure.cs @@ -40,6 +40,7 @@ public CosmosQueryStructure( { _context = context; SourceAlias = _containerAlias; + DatabaseObject.Name = _containerAlias; Init(parameters); } From 207bcfe9f6659db7faacfbe8a91113e12d12749a Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 20:29:16 -0800 Subject: [PATCH 19/31] No need for an override for SourceAlias --- src/Service/Resolvers/BaseQueryStructure.cs | 2 +- src/Service/Resolvers/CosmosQueryStructure.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Service/Resolvers/BaseQueryStructure.cs b/src/Service/Resolvers/BaseQueryStructure.cs index 0ad7a1bd96..2d3d3d7882 100644 --- a/src/Service/Resolvers/BaseQueryStructure.cs +++ b/src/Service/Resolvers/BaseQueryStructure.cs @@ -20,7 +20,7 @@ public class BaseQueryStructure /// /// The alias of the entity as used in the generated query. /// - public virtual string SourceAlias { get; set; } + public string SourceAlias { get; set; } /// /// The metadata provider of the respective database. diff --git a/src/Service/Resolvers/CosmosQueryStructure.cs b/src/Service/Resolvers/CosmosQueryStructure.cs index f11078af2d..4e75ea5564 100644 --- a/src/Service/Resolvers/CosmosQueryStructure.cs +++ b/src/Service/Resolvers/CosmosQueryStructure.cs @@ -19,8 +19,6 @@ public class CosmosQueryStructure : BaseQueryStructure private readonly IMiddlewareContext _context; private readonly string _containerAlias = "c"; - public override string SourceAlias { get => base.SourceAlias; set => base.SourceAlias = value; } - public bool IsPaginated { get; internal set; } public string Container { get; internal set; } From 572b3777bad1f2416e5f2e23f82c125a7b84bf0c Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 20:56:52 -0800 Subject: [PATCH 20/31] Remove unnecessary renames --- src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs | 2 +- src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs | 2 +- .../Services/MetadataProviders/PostgreSqlMetadataProvider.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs index 663bdb91aa..7360c0a9f5 100644 --- a/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -9,7 +9,7 @@ namespace Azure.DataApiBuilder.Service.Services { /// - /// MsSQL specific override for MetadataProvider. + /// MsSQL specific override for SqlMetadataProvider. /// All the method definitions from base class are sufficient /// this class is only created for symmetricity with MySql /// and ease of expanding the generics specific to MsSql. diff --git a/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs index 6838674266..9fb172141a 100644 --- a/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/MySqlMetadataProvider.cs @@ -12,7 +12,7 @@ namespace Azure.DataApiBuilder.Service.Services { /// - /// MySQL specific override for MetadataProvider + /// MySQL specific override for SqlMetadataProvider /// public class MySqlMetadataProvider : SqlMetadataProvider, ISqlMetadataProvider { diff --git a/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs index 7d9ad3098d..8734532b3f 100644 --- a/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs @@ -9,7 +9,7 @@ namespace Azure.DataApiBuilder.Service.Services { /// - /// PostgreSql specific override for MetadataProvider. + /// PostgreSql specific override for SqlMetadataProvider. /// All the method definitions from base class are sufficient /// this class is only created for symmetricity with MySql /// and ease of expanding the generics specific to PostgreSql. From 5e0aec5727a5624398daefbfb1fe54e5b0f44418 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 21:03:09 -0800 Subject: [PATCH 21/31] Remove SqlMetadataProvider changes for now --- .../MetadataProviders/SqlMetadataProvider.cs | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 1611a59df2..3a468a6130 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -32,9 +32,6 @@ public abstract class SqlMetadataProvider : private readonly Dictionary _entities; - // Dictionary mapping singular graphql types to entity name keys in the configuration - private readonly Dictionary _graphQLSingularTypeToEntityNameMap = new(); - // Contains all the referencing and referenced columns for each pair // of referencing and referenced tables. private Dictionary? _pairToFkDefinition; @@ -75,7 +72,6 @@ public SqlMetadataProvider( _runtimeConfigProvider = runtimeConfigProvider; _databaseType = runtimeConfig.DatabaseType; _entities = runtimeConfig.Entities; - _graphQLSingularTypeToEntityNameMap = runtimeConfig.GraphQLSingularTypeToEntityNameMap; _logger = logger; foreach (KeyValuePair entity in _entities) { @@ -202,25 +198,6 @@ public IDictionary GetEntityNamesAndDbObjects() return EntityToDatabaseObject; } - /// - public string GetEntityName(string graphQLType) - { - if (_entities.ContainsKey(graphQLType)) - { - return graphQLType; - } - - if (_graphQLSingularTypeToEntityNameMap.TryGetValue(graphQLType, out string? entityName)) - { - throw new DataApiBuilderException( - "GraphQL type doesn't match any entity name or singular type in the runtime config.", - System.Net.HttpStatusCode.BadRequest, - DataApiBuilderException.SubStatusCodes.BadRequest); - } - - return entityName!; - } - /// public async Task InitializeAsync() { @@ -1332,6 +1309,10 @@ public bool VerifyForeignKeyExistsInDB( /// public void SetPartitionKeyPath(string database, string container, string partitionKeyPath) => throw new NotImplementedException(); + + /// + public string GetEntityName(string graphQLType) + => throw new NotImplementedException(); } } From dc9e2fa598b971bf0d103c1fe70c03308b724fbc Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 10 Nov 2022 21:03:54 -0800 Subject: [PATCH 22/31] Fix spacing --- src/Service/Services/MetadataProviders/SqlMetadataProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 3a468a6130..17cb34035b 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1310,7 +1310,7 @@ public bool VerifyForeignKeyExistsInDB( public void SetPartitionKeyPath(string database, string container, string partitionKeyPath) => throw new NotImplementedException(); - /// + /// public string GetEntityName(string graphQLType) => throw new NotImplementedException(); } From 5ffdf5e8991e3797f7575953365118cf7fc051ab Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Fri, 11 Nov 2022 15:16:44 -0800 Subject: [PATCH 23/31] Remove unused private member --- src/Service/Models/GraphQLFilterParsers.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index ce89c6c121..de3a395eb5 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -17,15 +17,11 @@ public class GQLFilterParser { public static readonly string NullStringValue = "NULL"; - private readonly ISqlMetadataProvider _metadataProvider; - /// /// Constructor for the filter parser. /// - /// The metadata provider of the respective database. - public GQLFilterParser(ISqlMetadataProvider metadataProvider) + public GQLFilterParser() { - _metadataProvider = metadataProvider; } /// From 1a0f788ed18c19716a7cba1b29e49e8d5ad83fe8 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Sun, 13 Nov 2022 15:48:25 -0800 Subject: [PATCH 24/31] Fix constructor call --- src/Service.Tests/SqlTests/SqlTestBase.cs | 2 +- src/Service.Tests/Unittests/RestServiceUnitTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/SqlTests/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs index 5be14f2fd3..d066b22d3b 100644 --- a/src/Service.Tests/SqlTests/SqlTestBase.cs +++ b/src/Service.Tests/SqlTests/SqlTestBase.cs @@ -104,7 +104,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(); _httpContextAccessor.Setup(x => x.HttpContext.User).Returns(new ClaimsPrincipal()); - _gQLFilterParser = new(_sqlMetadataProvider); + _gQLFilterParser = new(); await ResetDbStateAsync(); // Execute additional queries, if any. diff --git a/src/Service.Tests/Unittests/RestServiceUnitTests.cs b/src/Service.Tests/Unittests/RestServiceUnitTests.cs index 9c3582dda3..5f637e4913 100644 --- a/src/Service.Tests/Unittests/RestServiceUnitTests.cs +++ b/src/Service.Tests/Unittests/RestServiceUnitTests.cs @@ -148,7 +148,7 @@ public static void InitializeTest(string path, string entityName) DefaultHttpContext context = new(); httpContextAccessor.Setup(_ => _.HttpContext).Returns(context); AuthorizationResolver authorizationResolver = new(runtimeConfigProvider, sqlMetadataProvider.Object, authLogger.Object); - GQLFilterParser gQLFilterParser = new(sqlMetadataProvider.Object); + GQLFilterParser gQLFilterParser = new(); SqlQueryEngine queryEngine = new( queryExecutor, queryBuilder, From c89b78ff632fdf87872f0595002d99906d55c63b Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 15 Nov 2022 15:49:49 -0800 Subject: [PATCH 25/31] Fix OdataVisitor unit tests --- src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs index 66876e6456..ca49f44b91 100644 --- a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs +++ b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs @@ -297,7 +297,11 @@ private static ODataASTVisitor CreateVisitor( }; FindRequestContext context = new(entityName, dbo, isList); - Mock structure = new(context, _sqlMetadataProvider, _runtimeConfigProvider); + Mock structure = new( + context, + _sqlMetadataProvider, + _runtimeConfigProvider, + new GQLFilterParser()); return new ODataASTVisitor(structure.Object, _sqlMetadataProvider); } From 29caeadaf9923e5f3e528b8613d324841c27d8e9 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 15 Nov 2022 16:09:26 -0800 Subject: [PATCH 26/31] Reset queryStructure to the caller's sourceName, sourceAlias after recursive call --- src/Service/Models/GraphQLFilterParsers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index de3a395eb5..5f5d59a81c 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -86,11 +86,13 @@ public Predicate Parse( if (!IsScalarType(filterInputObjectType.Name)) { queryStructure.DatabaseObject.Name = sourceName + "." + name; - queryStructure.SourceAlias = sourceName + "." + name; + queryStructure.SourceAlias = sourceAlias + "." + name; predicates.Push(new PredicateOperand(Parse(ctx, filterArgumentObject.Fields[name], subfields, queryStructure))); + queryStructure.DatabaseObject.Name = sourceName; + queryStructure.SourceAlias = sourceAlias; } else { From f3076a9ed19ab5211ffc79296ff22d839e1826ac Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 15 Nov 2022 16:23:27 -0800 Subject: [PATCH 27/31] Remove unnecessary constructor --- src/Service/Models/GraphQLFilterParsers.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index 5f5d59a81c..9082764e5c 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -17,13 +17,6 @@ public class GQLFilterParser { public static readonly string NullStringValue = "NULL"; - /// - /// Constructor for the filter parser. - /// - public GQLFilterParser() - { - } - /// /// Parse a predicate for a *FilterInput input type /// From 7eb07d7d4d4b226c01982e6ef3fa4037ee7cabfc Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 15 Nov 2022 16:33:21 -0800 Subject: [PATCH 28/31] Fix the mock constructor --- .../Unittests/ODataASTVisitorUnitTests.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs index ca49f44b91..9545c121a3 100644 --- a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs +++ b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs @@ -1,11 +1,15 @@ using System; using System.Threading.Tasks; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; +using Azure.DataApiBuilder.Service.Services; using Azure.DataApiBuilder.Service.Tests.SqlTests; +using Microsoft.Extensions.Logging; using Microsoft.OData; using Microsoft.OData.Edm; using Microsoft.OData.UriParser; @@ -296,10 +300,14 @@ private static ODataASTVisitor CreateVisitor( Name = tableName }; FindRequestContext context = new(entityName, dbo, isList); - + AuthorizationResolver authorizationResolver = new( + _runtimeConfigProvider, + _sqlMetadataProvider, + new Mock>().Object); Mock structure = new( context, _sqlMetadataProvider, + authorizationResolver, _runtimeConfigProvider, new GQLFilterParser()); return new ODataASTVisitor(structure.Object, _sqlMetadataProvider); From 9d5db6111e65dc50150b95522375fc2ae57aeb26 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Tue, 15 Nov 2022 16:34:10 -0800 Subject: [PATCH 29/31] Fix namespace --- src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs index 9545c121a3..c768903899 100644 --- a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs +++ b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs @@ -1,13 +1,11 @@ using System; using System.Threading.Tasks; -using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; -using Azure.DataApiBuilder.Service.Services; using Azure.DataApiBuilder.Service.Tests.SqlTests; using Microsoft.Extensions.Logging; using Microsoft.OData; From 23584efca43a493f55cc822b0ff14d4615f8ab07 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 17 Nov 2022 14:12:14 -0800 Subject: [PATCH 30/31] Apply suggestions from code review Co-authored-by: Sean Leonard --- src/Service/Resolvers/CosmosQueryEngine.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Service/Resolvers/CosmosQueryEngine.cs b/src/Service/Resolvers/CosmosQueryEngine.cs index d190bd9b62..71747083c9 100644 --- a/src/Service/Resolvers/CosmosQueryEngine.cs +++ b/src/Service/Resolvers/CosmosQueryEngine.cs @@ -42,6 +42,7 @@ public CosmosQueryEngine( _metadataStoreProvider = metadataStoreProvider; _queryBuilder = new CosmosQueryBuilder(); _gQLFilterParser = gQLFilterParser; + _authorizationResolver = authorizationResolver; } /// From 210d94ec26aca790b03982cb6b66c3521c1da06b Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Thu, 17 Nov 2022 14:22:53 -0800 Subject: [PATCH 31/31] Address review comments --- src/Service/Resolvers/CosmosQueryStructure.cs | 6 +- .../Sql Query Structures/SqlQueryStructure.cs | 4 +- src/Service/Resolvers/SqlMutationEngine.cs | 59 ++++++++++--------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/Service/Resolvers/CosmosQueryStructure.cs b/src/Service/Resolvers/CosmosQueryStructure.cs index 4e75ea5564..f7ed134630 100644 --- a/src/Service/Resolvers/CosmosQueryStructure.cs +++ b/src/Service/Resolvers/CosmosQueryStructure.cs @@ -57,7 +57,7 @@ private void Init(IDictionary queryParams) { FieldNode? fieldNode = ExtractItemsQueryField(selection.SyntaxNode); - if (fieldNode != null) + if (fieldNode is not null) { Columns.AddRange(fieldNode.SelectionSet!.Selections.Select(x => new LabelledColumn(tableSchema: string.Empty, tableName: SourceAlias, @@ -107,7 +107,7 @@ private void Init(IDictionary queryParams) { object? orderByObject = queryParams["orderBy"]; - if (orderByObject != null) + if (orderByObject is not null) { OrderByColumns = ProcessGraphQLOrderByArg((List)orderByObject); } @@ -119,7 +119,7 @@ private void Init(IDictionary queryParams) { object? filterObject = queryParams[QueryBuilder.FILTER_FIELD_NAME]; - if (filterObject != null) + if (filterObject is not null) { List filterFields = (List)filterObject; Predicates.Add( diff --git a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index c731758b56..dad0cd98b8 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -396,7 +396,7 @@ private SqlQueryStructure( { object? filterObject = queryParams[QueryBuilder.FILTER_FIELD_NAME]; - if (filterObject != null) + if (filterObject is not null) { List filterFields = (List)filterObject; Predicates.Add(GraphQLFilterParser.Parse( @@ -412,7 +412,7 @@ private SqlQueryStructure( { object? orderByObject = queryParams[QueryBuilder.ORDER_BY_FIELD_NAME]; - if (orderByObject != null) + if (orderByObject is not null) { OrderByColumns = ProcessGqlOrderByArg((List)orderByObject, queryArgumentSchemas[QueryBuilder.ORDER_BY_FIELD_NAME]); } diff --git a/src/Service/Resolvers/SqlMutationEngine.cs b/src/Service/Resolvers/SqlMutationEngine.cs index 6cfa4b0e79..0861aec793 100644 --- a/src/Service/Resolvers/SqlMutationEngine.cs +++ b/src/Service/Resolvers/SqlMutationEngine.cs @@ -372,13 +372,15 @@ private static OkObjectResult OkMutationResponse(Dictionary? re { case Operation.Insert: case Operation.Create: - SqlInsertStructure insertQueryStruct = context is null ? - new(entityName, - _sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - parameters) : - new(context, + SqlInsertStructure insertQueryStruct = context is null + ? new( + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + parameters) + : new( + context, entityName, _sqlMetadataProvider, _authorizationResolver, @@ -387,8 +389,8 @@ private static OkObjectResult OkMutationResponse(Dictionary? re queryParameters = insertQueryStruct.Parameters; break; case Operation.Update: - SqlUpdateStructure updateStructure = - new(entityName, + SqlUpdateStructure updateStructure = new( + entityName, _sqlMetadataProvider, _authorizationResolver, _gQLFilterParser, @@ -398,8 +400,8 @@ private static OkObjectResult OkMutationResponse(Dictionary? re queryParameters = updateStructure.Parameters; break; case Operation.UpdateIncremental: - SqlUpdateStructure updateIncrementalStructure = - new(entityName, + SqlUpdateStructure updateIncrementalStructure = new( + entityName, _sqlMetadataProvider, _authorizationResolver, _gQLFilterParser, @@ -414,8 +416,7 @@ private static OkObjectResult OkMutationResponse(Dictionary? re throw new ArgumentNullException("Context should not be null for a GraphQL operation."); } - SqlUpdateStructure updateGraphQLStructure = - new( + SqlUpdateStructure updateGraphQLStructure = new( context, entityName, _sqlMetadataProvider, @@ -493,8 +494,8 @@ private async Task?> { string queryString; Dictionary queryParameters; - SqlDeleteStructure deleteStructure = - new(entityName, + SqlDeleteStructure deleteStructure = new( + entityName, _sqlMetadataProvider, _authorizationResolver, _gQLFilterParser, @@ -540,25 +541,25 @@ private async Task?> if (operationType is Operation.Upsert) { - SqlUpsertQueryStructure upsertStructure = - new(entityName, - _sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - parameters, - incrementalUpdate: false); + SqlUpsertQueryStructure upsertStructure = new( + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + parameters, + incrementalUpdate: false); queryString = _queryBuilder.Build(upsertStructure); queryParameters = upsertStructure.Parameters; } else { - SqlUpsertQueryStructure upsertIncrementalStructure = - new(entityName, - _sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - parameters, - incrementalUpdate: true); + SqlUpsertQueryStructure upsertIncrementalStructure = new( + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + parameters, + incrementalUpdate: true); queryString = _queryBuilder.Build(upsertIncrementalStructure); queryParameters = upsertIncrementalStructure.Parameters; }