From f478fe29a839fe81b15647e9ccad5dbd90a01c87 Mon Sep 17 00:00:00 2001 From: Ryan Idrogo-Lam Date: Fri, 19 Dec 2025 01:38:13 -0500 Subject: [PATCH] fix(graphql-relational-transformer): filter key fields from authFilter in relational resolvers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When owner or other auth fields are part of the composite primary key, they're already used in the key condition expression. DynamoDB prohibits using primary key attributes in filter expressions, causing the error: "Filter Expression can only contain non-primary key attributes" This fix filters out any authFilter fields that overlap with the key condition fields (references) before adding them to the filter expression. Closes #3364 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../package.json | 1 + ...raphql-belongs-to-transformer.test.ts.snap | 22 +- ...s-many-references-transformer.test.ts.snap | 433 +++++++++++++++++- ...as-one-references-transformer.test.ts.snap | 220 ++++++++- ...ql-has-many-references-transformer.test.ts | 103 +++++ .../src/resolver/ddb-references-generator.ts | 63 ++- yarn.lock | 31 +- 7 files changed, 798 insertions(+), 75 deletions(-) diff --git a/packages/amplify-graphql-relational-transformer/package.json b/packages/amplify-graphql-relational-transformer/package.json index f514eda5b5..d8afd71566 100644 --- a/packages/amplify-graphql-relational-transformer/package.json +++ b/packages/amplify-graphql-relational-transformer/package.json @@ -40,6 +40,7 @@ "immer": "^9.0.12" }, "devDependencies": { + "@aws-amplify/graphql-auth-transformer": "4.2.4", "@aws-amplify/graphql-transformer-test-utils": "1.0.18" }, "peerDependencies": { diff --git a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-belongs-to-transformer.test.ts.snap b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-belongs-to-transformer.test.ts.snap index 9374eda04f..38e0e8448e 100644 --- a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-belongs-to-transformer.test.ts.snap +++ b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-belongs-to-transformer.test.ts.snap @@ -686,8 +686,17 @@ exports[`@belongsTo directive with RDS datasource set custom index name for hasM } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"blogId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -791,8 +800,17 @@ exports[`@belongsTo directive with RDS datasource set custom index name for hasO \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"blogId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" diff --git a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-many-references-transformer.test.ts.snap b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-many-references-transformer.test.ts.snap index b3f817bb58..a106f6ac04 100644 --- a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-many-references-transformer.test.ts.snap +++ b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-many-references-transformer.test.ts.snap @@ -313,8 +313,17 @@ exports[`has many query 2`] = ` } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"teamID\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -384,8 +393,17 @@ exports[`has many query 3`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamID\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -704,8 +722,17 @@ exports[`has many query with implicit IDs 2`] = ` } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"teamID\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -775,8 +802,17 @@ exports[`has many query with implicit IDs 3`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamID\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -807,8 +843,17 @@ exports[`has many references with multiple relationships to the same model 1`] = } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"primaryId1\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -882,8 +927,17 @@ exports[`has many references with multiple relationships to the same model 2`] = } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"primaryId2\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -953,8 +1007,17 @@ exports[`has many references with multiple relationships to the same model 3`] = \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryId1\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -981,8 +1044,17 @@ exports[`has many references with multiple relationships to the same model 4`] = \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryId2\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1017,8 +1089,17 @@ exports[`has many references with multiple relationships to the same model with } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"primaryPk1\\", \\"primarySk11\\", \\"primarySk12\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -1096,8 +1177,17 @@ exports[`has many references with multiple relationships to the same model with } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"primaryPk2\\", \\"primarySk21\\", \\"primarySk22\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -1169,8 +1259,17 @@ exports[`has many references with multiple relationships to the same model with \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.primarySk11}#\${ctx.source.primarySk12}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryPk1\\", \\"primarySk11\\", \\"primarySk12\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1199,8 +1298,17 @@ exports[`has many references with multiple relationships to the same model with \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.primarySk21}#\${ctx.source.primarySk22}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryPk2\\", \\"primarySk21\\", \\"primarySk22\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1263,8 +1371,17 @@ exports[`has many references with multiple sort keys 2`] = ` } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\", \\"teamOrganization\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -1336,8 +1453,17 @@ exports[`has many references with multiple sort keys 3`] = ` \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.teamMantra}#\${ctx.source.teamOrganization}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\", \\"teamOrganization\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1371,8 +1497,17 @@ exports[`has many references with partition key + sort key 1`] = ` } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -1444,8 +1579,17 @@ exports[`has many references with partition key + sort key 2`] = ` \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.teamMantra, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1473,8 +1617,17 @@ exports[`hasMany / hasOne - belongsTo across data source type boundary 1`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1505,8 +1658,17 @@ exports[`hasMany / hasOne - belongsTo across data source type boundary 2`] = ` } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"teamId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -1619,6 +1781,219 @@ $util.qr($lambdaInput.args.input.put(\\"id\\", $ctx.source.teamId)) ## [End] Invoke RDS Lambda data source. **" `; +exports[`hasMany with owner in composite primary key filters out owner from authFilter when owner is part of hasMany references 1`] = ` +"#if( $ctx.stash.deniedField ) + #set( $result = { + \\"items\\": [] +} ) + #return($result) +#end +#set( $partitionKeyValue = $util.defaultIfNull($ctx.stash.connectionAttributes.get(\\"owner\\"), $ctx.source.owner) ) +#if( $util.isNull($partitionKeyValue) ) + #set( $result = { + \\"items\\": [] +} ) + #return($result) +#else + #set( $limit = $util.defaultIfNull($context.args.limit, 100) ) + #set( $sortKeyValue0 = $util.defaultIfNull($ctx.stash.connectionAttibutes.get(\\"parentId\\"), $ctx.source.parentId) ) + #set( $query = { + \\"expression\\": \\"#partitionKey = :partitionKey AND #sortKey = :sortKey\\", + \\"expressionNames\\": { + \\"#partitionKey\\": \\"owner\\", + \\"#sortKey\\": \\"parentId\\" + }, + \\"expressionValues\\": { + \\":partitionKey\\": $util.dynamodb.toDynamoDB($partitionKeyValue), + \\":sortKey\\": $util.dynamodb.toDynamoDB(\\"\${sortKeyValue0}\\") + } +} ) + #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"owner\\", \\"parentId\\"] ) + #set( $filteredAuthFilter = {} ) + #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) + #if( !$util.isNullOrEmpty($args.filter) ) + #set( $filter = { + \\"and\\": [$filter, $args.filter] +} ) + #end + #else + #if( !$util.isNullOrEmpty($args.filter) ) + #set( $filter = $args.filter ) + #end + #end + #if( !$util.isNullOrEmpty($filter) ) + #set( $filterExpression = $util.parseJson($util.transform.toDynamoDBFilterExpression($filter)) ) + #if( !$util.isNullOrBlank($filterExpression.expression) ) + #if( $filterExpression.expressionValues.size() == 0 ) + $util.qr($filterExpression.remove(\\"expressionValues\\")) + #end + #set( $filter = $filterExpression ) + #end + #end +{ + \\"version\\": \\"2018-05-29\\", + \\"operation\\": \\"Query\\", + \\"query\\": $util.toJson($query), + \\"scanIndexForward\\": #if( $context.args.sortDirection ) + #if( $context.args.sortDirection == \\"ASC\\" ) +true + #else +false + #end + #else +true + #end, + \\"filter\\": #if( $filter ) +$util.toJson($filter) + #else +null + #end, + \\"limit\\": $limit, + \\"nextToken\\": #if( $context.args.nextToken ) +$util.toJson($context.args.nextToken) + #else +null + #end, + \\"index\\": \\"gsi-ParentModel.children\\" + } +#end" +`; + +exports[`hasMany with owner in composite primary key filters out owner from authFilter when owner is part of hasMany references 2`] = ` +"#if( $ctx.stash.deniedField ) + #return($util.toJson(null)) +#end +#set( $partitionKeyValue = $util.defaultIfNull($ctx.stash.connectionAttibutes.get(\\"owner\\"), $ctx.source.owner) ) +#if( $util.isNull($partitionKeyValue) || $util.isNull($ctx.source.parentId) ) + #return +#else + #set( $GetRequest = { + \\"version\\": \\"2018-05-29\\", + \\"operation\\": \\"Query\\" +} ) + $util.qr($GetRequest.put(\\"query\\", { + \\"expression\\": \\"#partitionKey = :partitionValue AND #sortKeyName = :sortKeyName\\", + \\"expressionNames\\": { + \\"#partitionKey\\": \\"owner\\", + \\"#sortKeyName\\": \\"parentId\\" + }, + \\"expressionValues\\": { + \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))), + \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.parentId, \\"___xamznone____\\"))) + } +})) + #set( $keyFields = [\\"owner\\", \\"parentId\\"] ) + #set( $filteredAuthFilter = {} ) + #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) + #end + $util.toJson($GetRequest) +#end" +`; + +exports[`hasMany with owner in composite primary key preserves non-key auth fields in filter when owner is in references 1`] = ` +"#if( $ctx.stash.deniedField ) + #set( $result = { + \\"items\\": [] +} ) + #return($result) +#end +#set( $partitionKeyValue = $util.defaultIfNull($ctx.stash.connectionAttributes.get(\\"owner\\"), $ctx.source.owner) ) +#if( $util.isNull($partitionKeyValue) ) + #set( $result = { + \\"items\\": [] +} ) + #return($result) +#else + #set( $limit = $util.defaultIfNull($context.args.limit, 100) ) + #set( $sortKeyValue0 = $util.defaultIfNull($ctx.stash.connectionAttibutes.get(\\"tenantId\\"), $ctx.source.tenantId) ) + #set( $query = { + \\"expression\\": \\"#partitionKey = :partitionKey AND #sortKey = :sortKey\\", + \\"expressionNames\\": { + \\"#partitionKey\\": \\"owner\\", + \\"#sortKey\\": \\"tenantId\\" + }, + \\"expressionValues\\": { + \\":partitionKey\\": $util.dynamodb.toDynamoDB($partitionKeyValue), + \\":sortKey\\": $util.dynamodb.toDynamoDB(\\"\${sortKeyValue0}\\") + } +} ) + #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"owner\\", \\"tenantId\\"] ) + #set( $filteredAuthFilter = {} ) + #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) + #if( !$util.isNullOrEmpty($args.filter) ) + #set( $filter = { + \\"and\\": [$filter, $args.filter] +} ) + #end + #else + #if( !$util.isNullOrEmpty($args.filter) ) + #set( $filter = $args.filter ) + #end + #end + #if( !$util.isNullOrEmpty($filter) ) + #set( $filterExpression = $util.parseJson($util.transform.toDynamoDBFilterExpression($filter)) ) + #if( !$util.isNullOrBlank($filterExpression.expression) ) + #if( $filterExpression.expressionValues.size() == 0 ) + $util.qr($filterExpression.remove(\\"expressionValues\\")) + #end + #set( $filter = $filterExpression ) + #end + #end +{ + \\"version\\": \\"2018-05-29\\", + \\"operation\\": \\"Query\\", + \\"query\\": $util.toJson($query), + \\"scanIndexForward\\": #if( $context.args.sortDirection ) + #if( $context.args.sortDirection == \\"ASC\\" ) +true + #else +false + #end + #else +true + #end, + \\"filter\\": #if( $filter ) +$util.toJson($filter) + #else +null + #end, + \\"limit\\": $limit, + \\"nextToken\\": #if( $context.args.nextToken ) +$util.toJson($context.args.nextToken) + #else +null + #end, + \\"index\\": \\"gsi-Tenant.projects\\" + } +#end" +`; + exports[`many to many query 1`] = ` "type Post { id: ID! @@ -2029,8 +2404,17 @@ exports[`supports recursive schemas 1`] = ` } } ) #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) + #set( $keyFields = [\\"parentId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - #set( $filter = $ctx.stash.authFilter ) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + #set( $filter = $filteredAuthFilter ) #if( !$util.isNullOrEmpty($args.filter) ) #set( $filter = { \\"and\\": [$filter, $args.filter] @@ -2100,8 +2484,17 @@ exports[`supports recursive schemas 2`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"parentId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" diff --git a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-one-references-transformer.test.ts.snap b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-one-references-transformer.test.ts.snap index 8bba467cab..3597c79ff8 100644 --- a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-one-references-transformer.test.ts.snap +++ b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-has-one-references-transformer.test.ts.snap @@ -310,8 +310,17 @@ exports[`has one query with implicit IDs 2`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamID\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -338,8 +347,17 @@ exports[`has one query with implicit IDs 3`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamID\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -367,8 +385,17 @@ exports[`has one references single partition key 1`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -395,8 +422,17 @@ exports[`has one references single partition key 2`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -424,8 +460,17 @@ exports[`has one references with multiple relationships to the same model 1`] = \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryId1\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -453,8 +498,17 @@ exports[`has one references with multiple relationships to the same model 2`] = \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryId2\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -481,8 +535,17 @@ exports[`has one references with multiple relationships to the same model 3`] = \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryId1\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -509,8 +572,17 @@ exports[`has one references with multiple relationships to the same model 4`] = \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryId2\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -540,8 +612,17 @@ exports[`has one references with multiple relationships to the same model with c \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.sk1}#\${ctx.source.sk2}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryPk1\\", \\"primarySk11\\", \\"primarySk12\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -571,8 +652,17 @@ exports[`has one references with multiple relationships to the same model with c \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.sk1}#\${ctx.source.sk2}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryPk2\\", \\"primarySk21\\", \\"primarySk22\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -601,8 +691,17 @@ exports[`has one references with multiple relationships to the same model with c \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.primarySk11}#\${ctx.source.primarySk12}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryPk1\\", \\"primarySk11\\", \\"primarySk12\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -631,8 +730,17 @@ exports[`has one references with multiple relationships to the same model with c \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.primarySk21}#\${ctx.source.primarySk22}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"primaryPk2\\", \\"primarySk21\\", \\"primarySk22\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1389,8 +1497,17 @@ $util.qr($ctx.result.put(\\"__operation\\", \\"Mutation\\")) \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.teamMantra}#\${ctx.source.teamOrganization}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\", \\"teamOrganization\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end", @@ -1889,8 +2006,17 @@ $util.toJson(null) \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.mantra}#\${ctx.source.organization}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\", \\"teamOrganization\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end", @@ -1938,8 +2064,17 @@ exports[`has one references with multiple sort keys 2`] = ` \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.mantra}#\${ctx.source.organization}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\", \\"teamOrganization\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1968,8 +2103,17 @@ exports[`has one references with multiple sort keys 3`] = ` \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank(\\"\${ctx.source.teamMantra}#\${ctx.source.teamOrganization}\\", \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\", \\"teamOrganization\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -1999,8 +2143,17 @@ exports[`has one references with partition key + sort key 1`] = ` \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.mantra, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -2029,8 +2182,17 @@ exports[`has one references with partition key + sort key 2`] = ` \\":sortKeyName\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.teamMantra, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"teamId\\", \\"teamMantra\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -2058,8 +2220,17 @@ exports[`supports recursive schemas 1`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"parentId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" @@ -2086,8 +2257,17 @@ exports[`supports recursive schemas 2`] = ` \\":partitionValue\\": $util.parseJson($util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($partitionKeyValue, \\"___xamznone____\\"))) } })) + #set( $keyFields = [\\"parentId\\"] ) + #set( $filteredAuthFilter = {} ) #if( !$util.isNullOrEmpty($ctx.stash.authFilter) ) - $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($ctx.stash.authFilter)))) + #foreach( $authKey in $ctx.stash.authFilter.keySet() ) + #if( !$keyFields.contains($authKey) ) + $util.qr($filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))) + #end + #end + #end + #if( !$util.isNullOrEmpty($filteredAuthFilter) ) + $util.qr($GetRequest.put(\\"filter\\", $util.parseJson($util.transform.toDynamoDBFilterExpression($filteredAuthFilter)))) #end $util.toJson($GetRequest) #end" diff --git a/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-has-many-references-transformer.test.ts b/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-has-many-references-transformer.test.ts index b52f98747d..c34a4dbd4c 100644 --- a/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-has-many-references-transformer.test.ts +++ b/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-has-many-references-transformer.test.ts @@ -1,6 +1,8 @@ +import { AuthTransformer } from '@aws-amplify/graphql-auth-transformer'; import { IndexTransformer, PrimaryKeyTransformer } from '@aws-amplify/graphql-index-transformer'; import { ModelTransformer } from '@aws-amplify/graphql-model-transformer'; import { ConflictHandlerType, DDB_DEFAULT_DATASOURCE_STRATEGY, validateModelSchema } from '@aws-amplify/graphql-transformer-core'; +import { AppSyncAuthConfiguration } from '@aws-amplify/graphql-transformer-interfaces'; import { Kind, parse } from 'graphql'; import { mockSqlDataSourceStrategy, testTransform } from '@aws-amplify/graphql-transformer-test-utils'; import { BelongsToTransformer, HasManyTransformer, HasOneTransformer } from '..'; @@ -740,3 +742,104 @@ test('supports recursive schemas', () => { expect(out.resolvers['TreeNode.parent.req.vtl']).toMatchSnapshot(); expect(out.resolvers['TreeNode.parent.req.vtl']).toContain('connectionAttibutes.get("parentId")'); }); + +describe('hasMany with owner in composite primary key', () => { + const cognitoUserPoolsAuthConfig: AppSyncAuthConfiguration = { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }; + + test('filters out owner from authFilter when owner is part of hasMany references', () => { + const inputSchema = ` + type ParentModel @model @auth(rules: [{ allow: owner, ownerField: "owner" }]) { + owner: String! @primaryKey(sortKeyFields: ["parentId"]) + parentId: ID! + children: [ChildModel] @hasMany(references: ["owner", "parentId"]) + } + + type ChildModel @model @auth(rules: [{ allow: owner, ownerField: "owner" }]) { + owner: String! @primaryKey(sortKeyFields: ["parentId", "childId"]) + parentId: ID! + childId: ID! + name: String + parent: ParentModel @belongsTo(references: ["owner", "parentId"]) + } + `; + + const out = testTransform({ + schema: inputSchema, + authConfig: cognitoUserPoolsAuthConfig, + transformers: [ + new ModelTransformer(), + new PrimaryKeyTransformer(), + new HasManyTransformer(), + new BelongsToTransformer(), + new AuthTransformer(), + ], + }); + + expect(out).toBeDefined(); + const schema = parse(out.schema); + validateModelSchema(schema); + + expect(out.resolvers['ParentModel.children.req.vtl']).toBeDefined(); + expect(out.resolvers['ParentModel.children.req.vtl']).toMatchSnapshot(); + + const hasManyResolver = out.resolvers['ParentModel.children.req.vtl']; + expect(hasManyResolver).toContain('#set( $keyFields = ["owner", "parentId"] )'); + expect(hasManyResolver).toContain('#set( $filteredAuthFilter = {} )'); + expect(hasManyResolver).toContain('#if( !$keyFields.contains($authKey) )'); + expect(hasManyResolver).toContain('#if( !$util.isNullOrEmpty($filteredAuthFilter) )'); + expect(hasManyResolver).toContain('#set( $filter = $filteredAuthFilter )'); + + expect(out.resolvers['ChildModel.parent.req.vtl']).toBeDefined(); + expect(out.resolvers['ChildModel.parent.req.vtl']).toMatchSnapshot(); + const belongsToResolver = out.resolvers['ChildModel.parent.req.vtl']; + expect(belongsToResolver).toContain('#set( $keyFields = ["owner", "parentId"] )'); + expect(belongsToResolver).toContain('#set( $filteredAuthFilter = {} )'); + }); + + test('preserves non-key auth fields in filter when owner is in references', () => { + const inputSchema = ` + type Tenant @model @auth(rules: [{ allow: owner, ownerField: "owner" }, { allow: groups, groups: ["Admin"] }]) { + owner: String! @primaryKey(sortKeyFields: ["tenantId"]) + tenantId: ID! + projects: [Project] @hasMany(references: ["owner", "tenantId"]) + } + + type Project @model @auth(rules: [{ allow: owner, ownerField: "owner" }, { allow: groups, groups: ["Admin"] }]) { + owner: String! @primaryKey(sortKeyFields: ["tenantId", "projectId"]) + tenantId: ID! + projectId: ID! + name: String + tenant: Tenant @belongsTo(references: ["owner", "tenantId"]) + } + `; + + const out = testTransform({ + schema: inputSchema, + authConfig: cognitoUserPoolsAuthConfig, + transformers: [ + new ModelTransformer(), + new PrimaryKeyTransformer(), + new HasManyTransformer(), + new BelongsToTransformer(), + new AuthTransformer(), + ], + }); + + expect(out).toBeDefined(); + const schema = parse(out.schema); + validateModelSchema(schema); + + expect(out.resolvers['Tenant.projects.req.vtl']).toBeDefined(); + expect(out.resolvers['Tenant.projects.req.vtl']).toMatchSnapshot(); + + const hasManyResolver = out.resolvers['Tenant.projects.req.vtl']; + expect(hasManyResolver).toContain('#set( $keyFields = ["owner", "tenantId"] )'); + expect(hasManyResolver).toContain('#foreach( $authKey in $ctx.stash.authFilter.keySet() )'); + expect(hasManyResolver).toContain('$filteredAuthFilter.put($authKey, $ctx.stash.authFilter.get($authKey))'); + }); +}); diff --git a/packages/amplify-graphql-relational-transformer/src/resolver/ddb-references-generator.ts b/packages/amplify-graphql-relational-transformer/src/resolver/ddb-references-generator.ts index 4bd850322a..3448bb81d8 100644 --- a/packages/amplify-graphql-relational-transformer/src/resolver/ddb-references-generator.ts +++ b/packages/amplify-graphql-relational-transformer/src/resolver/ddb-references-generator.ts @@ -114,13 +114,24 @@ export class DDBRelationalReferencesResolverGenerator extends DDBRelationalResol set(ref('query'), this.makeQueryExpression(references)), ]; - // add setup filter to query + // Filter out authFilter fields already in key condition to avoid DynamoDB error setup.push( setArgs, - ifElse( + set(ref('keyFields'), list(references.map((f) => str(f)))), + set(ref('filteredAuthFilter'), obj({})), + iff( not(isNullOrEmpty(authFilter)), + forEach(ref('authKey'), ref('ctx.stash.authFilter.keySet()'), [ + iff( + not(ref('keyFields.contains($authKey)')), + qref(methodCall(ref('filteredAuthFilter.put'), ref('authKey'), methodCall(ref('ctx.stash.authFilter.get'), ref('authKey')))), + ), + ]), + ), + ifElse( + not(isNullOrEmpty(ref('filteredAuthFilter'))), compoundExpression([ - set(ref('filter'), authFilter), + set(ref('filter'), ref('filteredAuthFilter')), iff(not(isNullOrEmpty(ref('args.filter'))), set(ref('filter'), obj({ and: list([ref('filter'), ref('args.filter')]) }))), ]), iff(not(isNullOrEmpty(ref('args.filter'))), set(ref('filter'), ref('args.filter'))), @@ -291,13 +302,34 @@ export class DDBRelationalReferencesResolverGenerator extends DDBRelationalResol }), ), ), + // Filter out authFilter fields already in key condition to avoid DynamoDB error + set(ref('keyFields'), list(references.map((f) => str(f)))), + set(ref('filteredAuthFilter'), obj({})), iff( not(isNullOrEmpty(authFilter)), + forEach(ref('authKey'), ref('ctx.stash.authFilter.keySet()'), [ + iff( + not(ref('keyFields.contains($authKey)')), + qref( + methodCall( + ref('filteredAuthFilter.put'), + ref('authKey'), + methodCall(ref('ctx.stash.authFilter.get'), ref('authKey')), + ), + ), + ), + ]), + ), + iff( + not(isNullOrEmpty(ref('filteredAuthFilter'))), qref( methodCall( ref('GetRequest.put'), str('filter'), - methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), authFilter)), + methodCall( + ref('util.parseJson'), + methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('filteredAuthFilter')), + ), ), ), ), @@ -418,13 +450,34 @@ export class DDBRelationalReferencesResolverGenerator extends DDBRelationalResol }), ), ), + // Filter out authFilter fields already in key condition to avoid DynamoDB error + set(ref('keyFields'), list(references.map((f) => str(f)))), + set(ref('filteredAuthFilter'), obj({})), iff( not(isNullOrEmpty(authFilter)), + forEach(ref('authKey'), ref('ctx.stash.authFilter.keySet()'), [ + iff( + not(ref('keyFields.contains($authKey)')), + qref( + methodCall( + ref('filteredAuthFilter.put'), + ref('authKey'), + methodCall(ref('ctx.stash.authFilter.get'), ref('authKey')), + ), + ), + ), + ]), + ), + iff( + not(isNullOrEmpty(ref('filteredAuthFilter'))), qref( methodCall( ref('GetRequest.put'), str('filter'), - methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), authFilter)), + methodCall( + ref('util.parseJson'), + methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('filteredAuthFilter')), + ), ), ), ), diff --git a/yarn.lock b/yarn.lock index 2e1cc0c6f9..c51a040020 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22085,16 +22085,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22216,7 +22207,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22237,13 +22228,6 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -23277,7 +23261,7 @@ workerpool@^6.5.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -23295,15 +23279,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7, wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"