Repository: sid88in/serverless-appsync-plugin
Pull request #604 (2023-06-13 19:57:12)

feat: add visibility configuration
Added Visibility property, to add support for Private mode : https://aws.amazon.com/blogs/mobile/introducing-private-apis-on-aws-appsync/


https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-graphqlapi.html#cfn-appsync-graphqlapi-visibility

N3RG4L: @bboure  thank you for your fast answer, and for the guidance.
I just added the requirements you mentioned. I hope I missed nothing else

N3RG4L: added the requested changes 

Patch:
diff --git a/README.md b/README.md
index f6bbba5..62096a0 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ appSync:
 - [Authentication](doc/authentication.md)
 - [API keys](doc/API-keys.md)
 - [Custom Domain](doc/custom-domain.md)
-- [VTL Template Substitutions](doc/substitutions.md)
+- [Variable Substitutions](doc/substitutions.md)
 - [Caching](doc/caching.md)
 - [Web Application Firewall (WAF)](doc/WAF.md)
 
@@ -65,7 +65,7 @@ This plugin adds some useful CLI commands. See [CLI commands documentation](doc/
 This plugin exports some handy variables that you can use in your yml files to reference some values generated by CloudFormation:
 
 - `${appsync:id}`: The id of the AppSync API
-- `${appsync:url}`: The ARN of the AppSync API
+- `${appsync:url}`: The URL of the AppSync API
 - `${appsync:arn}`: The ARN of the AppSync API
 - `${appsync:apiKey.[NAME]}`: An API key
 
diff --git a/doc/general-config.md b/doc/general-config.md
index 84dee06..26e7990 100644
--- a/doc/general-config.md
+++ b/doc/general-config.md
@@ -49,6 +49,7 @@ appSync:
 - `waf`: See [Web Application Firefall](WAF.md)
 - `logging`: See [Logging](#Logging)
 - `xrayEnabled`: Boolean. Enable or disable X-Ray tracing.
+- `visibility`: Optional. `GLOBAL` or `PRIVATE`. Defaults to `GLOBAL`.
 - `tags`: A key-value pair for tagging this AppSync API
 
 ## Schema
diff --git a/doc/pipeline-functions.md b/doc/pipeline-functions.md
index abaacb8..02f3750 100644
--- a/doc/pipeline-functions.md
+++ b/doc/pipeline-functions.md
@@ -22,7 +22,7 @@ appSync:
 - `request`: The path to the VTL request mapping template file, relative to `serverless.yml`.
 - `response`: The path to the VTL response mapping template file, relative to `serverless.yml`.
 - `maxBatchSize`: The maximum [batch size](https://aws.amazon.com/blogs/mobile/introducing-configurable-batching-size-for-aws-appsync-lambda-resolvers/) to use (only available for AWS Lambda DataSources)
-- `substitutions`: See [VTL template substitutions](substitutions.md)
+- `substitutions`: See [Variable Substitutions](substitutions.md)
 - `sync`: [See SyncConfig](syncConfig.md)
 
 ## JavaScript vs VTL vs Direct Lambda
diff --git a/doc/resolvers.md b/doc/resolvers.md
index 0651196..bc9d20a 100644
--- a/doc/resolvers.md
+++ b/doc/resolvers.md
@@ -24,11 +24,12 @@ appSync:
 - `field`: The Field in the schema this resolver is attached to. Optional if specified in the configuration key.
 - `kind`: The kind of resolver. Can be `UNIT` or `PIPELINE` ([see below](#PIPELINE-resolvers)). Defaults to `PIPELINE`
 - `dataSource`: The name of the [dataSource](dataSources.md) this resolver uses.
+- `functions`: For pipeline resolvers ([see below](#PIPELINE-resolvers)) the array of functions to run in sequence
 - `maxBatchSize`: The maximum [batch size](https://aws.amazon.com/blogs/mobile/introducing-configurable-batching-size-for-aws-appsync-lambda-resolvers/) to use (only available for AWS Lambda DataSources)
 - `code`: The path of the JavaScript resolver handler file, relative to `serverless.yml`. If not specified, a [minimalistic default](#javascript-vs-vtl) is used.
 - `request`: The path to the VTL request mapping template file, relative to `serverless.yml`.
 - `response`: The path to the VTL response mapping template file, relative to `serverless.yml`.
-- `substitutions`: See [VTL template substitutions](substitutions.md)
+- `substitutions`: See [Variable Substitutions](substitutions.md)
 - `caching`: [See below](#Caching)
 - `sync`: [See SyncConfig](syncConfig.md)
 
@@ -64,7 +65,6 @@ appSync:
 
   resolvers:
     Query.user:
-      dataSource: my-table
       functions:
         - function1
         - function2
diff --git a/doc/substitutions.md b/doc/substitutions.md
index 5d2a857..794953b 100644
--- a/doc/substitutions.md
+++ b/doc/substitutions.md
@@ -1,10 +1,16 @@
 # Substitutions
 
-Substitutions is a feature that allows you to replace some variables in your VTL mapping templates with dynamic values.
+`Substitutions` is a feature that allows you to replace some variables in your VTL mapping templates or JS resolvers with dynamic values.
 
 They are usually useful to replace parts of the template with resource names or ARNs coming from your IaC, such as a DynamoDB table names, for example; or the stage name, region name, etc.
 
-## Quick start
+## Usage
+
+Substitutions are defined as key-value pairs under the `appSync.substitutions`, `appSync.resolvers.[resolverName].substitutions` or `appSync.pipelineFunctions.[functionName].substitutions` attributes.
+
+Global substitutions are available globally to all mapping templates. Resolver and Pipeline function substitutions are only available where they are defined. Resolver and Pipeline function substitutions take precedence over global substitutions (values will be overwritten).
+
+Once defined, you can then use them within the mapping templates as if they were VTL variables. For JS resolvers you'll need to add the variable name as a string padded by `#`. At deployment time, variables will be substituted with their corresponding value.
 
 ```yaml
 appSync:
@@ -21,43 +27,30 @@ appSync:
         someVariable: someValue
 ```
 
-```vtl
-#set($postsdata = [])
-#foreach($item in ${ctx.args.posts})
-    $util.qr($postsdata.add($util.dynamodb.toMapValues($item)))
-#end
-{
-    "version" : "2018-05-29",
-    "operation" : "BatchPutItem",
-    "tables" : {
-        "${postsTable}": $utils.toJson($postsdata)
-    }
-}
-```
-
-## Usage
-
-Substitutions are defined as key-value pairs under the `appSync.substitutions`, `appSync.resolvers.[resolverName].substitutions` or `appSync.pipelineFunctions.[functionName].substitutions` attributes.
-
-Global substitutions are available globally to all mapping templates. Resolver and Pipeline function substitutions are only available where they are defined. Resolver and Pipeline function substitutions take precedence over global substitutions (values will be overwritten).
-
-Once defined, you can then use them within the mapping templates as if they were VTL variables. At deployment time, variables will be substituted with their corresponding value.
-
-```yaml
-appSync:
-  name: my-api
-  substitutions:
-    postsTable: !Ref Posts
-```
-
-In the mapping template:
-
-```vtl
-{
-    "version" : "2018-05-29",
-    "operation" : "BatchPutItem",
-    "tables" : {
-        "${postsTable}": [...]
-    }
-}
-```
+<details open>
+  <summary>VTL mapping template</summary>
+
+  ```vtl
+  {
+      "version" : "2018-05-29",
+      "operation" : "BatchPutItem",
+      "tables" : {
+          "${postsTable}": [...]
+      }
+  }
+  ```
+</details>
+
+<details open>
+  <summary>JS Resolvers</summary>
+  
+  ```js
+  const tableName = '#postsTable#';
+  return {
+    operation: "BatchGetItem",
+    tables: {
+      [tableName]: { keys },
+    },
+  };
+  ```
+</details>
diff --git a/src/resources/Api.ts b/src/resources/Api.ts
index 691d108..c1ea0a2 100644
--- a/src/resources/Api.ts
+++ b/src/resources/Api.ts
@@ -74,6 +74,7 @@ export class Api {
       Type: 'AWS::AppSync::GraphQLApi',
       Properties: {
         Name: this.config.name,
+        Visibility: this.config.visibility || 'GLOBAL',
         XrayEnabled: this.config.xrayEnabled || false,
         Tags: this.getTagsConfig(),
       },
diff --git a/src/types/plugin.ts b/src/types/plugin.ts
index 84d6deb..ea56c8b 100644
--- a/src/types/plugin.ts
+++ b/src/types/plugin.ts
@@ -16,6 +16,7 @@ export type AppSyncConfig = {
   caching?: CachingConfig;
   waf?: WafConfig;
   tags?: Record<string, string>;
+  visibility?: 'GLOBAL' | 'PRIVATE';
 };
 
 export type IamStatement = {
diff --git a/src/validation.ts b/src/validation.ts
index 6c10beb..a187049 100644
--- a/src/validation.ts
+++ b/src/validation.ts
@@ -678,6 +678,11 @@ export const appSyncSchema = {
       },
     },
     xrayEnabled: { type: 'boolean' },
+    visibility: {
+      type: 'string',
+      enum: ['GLOBAL', 'PRIVATE'],
+      errorMessage: 'must be "GLOBAL" or "PRIVATE"',
+    },
     substitutions: { $ref: '#/definitions/substitutions' },
     waf: {
       type: 'object',


Test patch:
diff --git a/src/__tests__/__snapshots__/api.test.ts.snap b/src/__tests__/__snapshots__/api.test.ts.snap
index 37d4314..b160f5a 100644
--- a/src/__tests__/__snapshots__/api.test.ts.snap
+++ b/src/__tests__/__snapshots__/api.test.ts.snap
@@ -27,6 +27,7 @@ Object {
           "Value": "Dev",
         },
       ],
+      "Visibility": "GLOBAL",
       "XrayEnabled": false,
     },
     "Type": "AWS::AppSync::GraphQLApi",
@@ -64,6 +65,7 @@ Object {
           "Value": "Dev",
         },
       ],
+      "Visibility": "GLOBAL",
       "XrayEnabled": false,
     },
     "Type": "AWS::AppSync::GraphQLApi",
diff --git a/src/__tests__/api.test.ts b/src/__tests__/api.test.ts
index 9f8c19b..28cba2d 100644
--- a/src/__tests__/api.test.ts
+++ b/src/__tests__/api.test.ts
@@ -22,6 +22,35 @@ describe('Api', () => {
                   "Value": "Dev",
                 },
               ],
+              "Visibility": "GLOBAL",
+              "XrayEnabled": false,
+            },
+            "Type": "AWS::AppSync::GraphQLApi",
+          },
+        }
+      `);
+    });
+
+    it('should compile the Api Resource for a private endpoint', () => {
+      const api = new Api(
+        given.appSyncConfig({
+          visibility: 'PRIVATE',
+        }),
+        plugin,
+      );
+      expect(api.compileEndpoint()).toMatchInlineSnapshot(`
+        Object {
+          "GraphQlApi": Object {
+            "Properties": Object {
+              "AuthenticationType": "API_KEY",
+              "Name": "MyApi",
+              "Tags": Array [
+                Object {
+                  "Key": "stage",
+                  "Value": "Dev",
+                },
+              ],
+              "Visibility": "PRIVATE",
               "XrayEnabled": false,
             },
             "Type": "AWS::AppSync::GraphQLApi",
@@ -63,6 +92,7 @@ describe('Api', () => {
                   "Value": "Dev",
                 },
               ],
+              "Visibility": "GLOBAL",
               "XrayEnabled": false,
             },
             "Type": "AWS::AppSync::GraphQLApi",
@@ -168,6 +198,7 @@ describe('Api', () => {
                 "DefaultAction": "ALLOW",
                 "UserPoolId": "pool123",
               },
+              "Visibility": "GLOBAL",
               "XrayEnabled": false,
             },
             "Type": "AWS::AppSync::GraphQLApi",
diff --git a/src/__tests__/validation/__snapshots__/base.test.ts.snap b/src/__tests__/validation/__snapshots__/base.test.ts.snap
index 3406190..c0639dc 100644
--- a/src/__tests__/validation/__snapshots__/base.test.ts.snap
+++ b/src/__tests__/validation/__snapshots__/base.test.ts.snap
@@ -54,5 +54,7 @@ exports[`Valdiation Waf Invalid should validate a Throttle limit 1`] = `
 exports[`Valdiation should validate  1`] = `
 ": must have required property 'name'
 : must have required property 'authentication'
-/unknownPorp: invalid (unknown) property"
+/unknownPorp: invalid (unknown) property
+/xrayEnabled: must be boolean
+/visibility: must be \\"GLOBAL\\" or \\"PRIVATE\\""
 `;
diff --git a/src/__tests__/validation/base.test.ts b/src/__tests__/validation/base.test.ts
index 1d6ff8d..bd9b7ee 100644
--- a/src/__tests__/validation/base.test.ts
+++ b/src/__tests__/validation/base.test.ts
@@ -4,8 +4,21 @@ import { basicConfig } from '../basicConfig';
 
 describe('Valdiation', () => {
   it('should validate ', () => {
+    expect(
+      validateConfig({
+        ...basicConfig,
+        visibility: 'GLOBAL',
+        xrayEnabled: true,
+        tags: {
+          foo: 'bar',
+        },
+      }),
+    ).toBe(true);
+
     expect(function () {
       validateConfig({
+        visibility: 'FOO',
+        xrayEnabled: 'BAR',
         unknownPorp: 'foo',
       });
     }).toThrowErrorMatchingSnapshot();
@@ -55,6 +68,7 @@ describe('Valdiation', () => {
               retentionInDays: 'bar',
               excludeVerboseContent: 'buzz',
               loggingRoleArn: 123,
+              visibility: 'FOO',
             },
           },
         },