diff --git a/the-simple-graphql-service/csharp/.gitignore b/the-simple-graphql-service/csharp/.gitignore new file mode 100644 index 00000000..f555633e --- /dev/null +++ b/the-simple-graphql-service/csharp/.gitignore @@ -0,0 +1,342 @@ +# CDK asset staging directory +.cdk.staging +cdk.out + +# Created by https://www.gitignore.io/api/csharp + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + + +# End of https://www.gitignore.io/api/csharp \ No newline at end of file diff --git a/the-simple-graphql-service/csharp/README.md b/the-simple-graphql-service/csharp/README.md new file mode 100644 index 00000000..50b36d45 --- /dev/null +++ b/the-simple-graphql-service/csharp/README.md @@ -0,0 +1,102 @@ +# The Simple GraphQL Service + +![architecture](../img/architecture.png) + +This is an example CDK stack to deploy The Simple [GraphQL](https://graphql.org/) Service inspired by [Thorsten Hoeger](https://twitter.com/hoegertn)'s contributions to the [CDK AppSync Module example]( https://docs.aws.amazon.com/cdk/api/latest/docs/aws-appsync-readme.html#usage-example) + +An advanced version of this pattern was talked about by [Heitor Lessa](https://twitter.com/heitor_lessa) at re:Invent 2019 as "The Cherry Pick". + +* [Youtube Recording](https://www.youtube.com/watch?v=9IYpGTS7Jy0) +* [Static Slides](https://d1.awsstatic.com/events/reinvent/2019/REPEAT_3_Serverless_architectural_patterns_and_best_practices_ARC307-R3.pdf) + +This is the most basic of [AppSync](https://aws.amazon.com/appsync/) implementations and would have to be hardened before production use. e.g. cognito user pools configured + +**Note, never print your API Key to the logs in a production system. This was added to make learning AppSync and GraphQL easier. If you want to use this pattern in a production system remove the two cloudformation outputs** + +### Testing + +1. After CDK Deploy, capture outputs from the log. + +```json +TheSimpleGraphqlServiceStack: deploying... +TheSimpleGraphqlServiceStack: creating CloudFormation changeset... + + TheSimpleGraphqlServiceStack + +Outputs: +TheSimpleGraphqlServiceStack.APIKey = +TheSimpleGraphqlServiceStack.Endpoint = https://.appsync-api.us-east-1.amazonaws.com/graphql +``` + +2. Setup Postman as outlined in Postman's [Using GraphQL Instructions](https://learning.postman.com/docs/postman/sending-api-requests/graphql/) + +3. Set POST request URL, x-api-key and Content-Type +* x-api-key = 'your API Key' +* Content-Type = application/graphql + +![postman](../img/postman-headers.png) + +4. Execute Mutations and Queries to exercice resolvers to dynamo and Lambda datasources + +![postman](../img/postman-queries.png) + +```json +// Inserts to Dynamo +mutation add { + addCustomer(customer: { name: "CDKPatterns"}) { + id + name + } +} + +// Queries the Dynamo DB +query getCustomers { + getCustomers{id name} +} + +// Executes the Lambda +query getLoyalty { + getLoyaltyLevel { level } +} +``` + +## Useful commands + +* `dotnet build src` compile this app +* `cdk deploy` deploy this stack to your default AWS account/region +* `cdk diff` compare deployed stack with current state +* `cdk synth` emits the synthesized CloudFormation template + +## Deploy with AWS Cloud9 + +* Create an **Ubuntu** AWS Cloud9 EC2 development environment +* Add the Microsoft repository + ``` + wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + ``` + ``` + sudo dpkg -i packages-microsoft-prod.deb + ``` +* Install the .NET Core SDK + ``` + sudo apt-get update; \ + sudo apt-get install -y apt-transport-https && \ + sudo apt-get update && \ + sudo apt-get install -y dotnet-sdk-3.1 + ``` +* Clone the CDK Patterns repo + ``` + git clone https://github.com/cdk-patterns/serverless.git + ``` +* Change directory + ``` + cd serverless/the-efs-lambda/csharp + ``` +* Build the project to see if .NET Core has been setup correctly (optional) + ``` + dotnet build src + ``` +* Deploy the stack + ``` + cdk deploy + ``` \ No newline at end of file diff --git a/the-simple-graphql-service/csharp/cdk.json b/the-simple-graphql-service/csharp/cdk.json new file mode 100644 index 00000000..ad084594 --- /dev/null +++ b/the-simple-graphql-service/csharp/cdk.json @@ -0,0 +1,11 @@ +{ + "app": "dotnet run -p src/TheSimpleGraphqlService/TheSimpleGraphqlService.csproj", + "context": { + "@aws-cdk/core:enableStackNameDuplicates": "true", + "aws-cdk:enableDiffNoFail": "true", + "@aws-cdk/core:stackRelativeExports": "true", + "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, + "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, + "@aws-cdk/aws-kms:defaultKeyPolicies": true + } +} diff --git a/the-simple-graphql-service/csharp/lambda_fns/loyalty.js b/the-simple-graphql-service/csharp/lambda_fns/loyalty.js new file mode 100644 index 00000000..4be794d1 --- /dev/null +++ b/the-simple-graphql-service/csharp/lambda_fns/loyalty.js @@ -0,0 +1,18 @@ +"use strict"; +const { Lambda } = require('aws-sdk'); +exports.handler = async function (event) { + console.log("get loyalty request:", JSON.stringify(event, undefined, 2)); + let loyaltylevel = "Silver"; + // return response back to upstream caller + return sendRes(200, loyaltylevel); +}; +const sendRes = (status, loyaltylevel) => { + var response = { + statusCode: status, + headers: { + "Content-Type": "application/json" + }, + level: loyaltylevel + }; + return response; +}; \ No newline at end of file diff --git a/the-simple-graphql-service/csharp/schema/schema.graphql b/the-simple-graphql-service/csharp/schema/schema.graphql new file mode 100644 index 00000000..ba36f7fc --- /dev/null +++ b/the-simple-graphql-service/csharp/schema/schema.graphql @@ -0,0 +1,35 @@ +type Customer { + id: String! + name: String! +} + +input SaveCustomerInput { + name: String! +} + +type Order { + customer: String! + order: String! +} + +type Loyalty { + level: String +} + +type Query { + getCustomers: [Customer] + getCustomer(id: String): Customer + getLoyaltyLevel(id: String): Loyalty +} + +input FirstOrderInput { + product: String! + quantity: Int! +} + +type Mutation { + addCustomer(customer: SaveCustomerInput!): Customer + saveCustomer(id: String!, customer: SaveCustomerInput!): Customer + removeCustomer(id: String!): Customer + saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order +} \ No newline at end of file diff --git a/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService.sln b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService.sln new file mode 100644 index 00000000..2c730475 --- /dev/null +++ b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TheSimpleGraphqlService", "TheSimpleGraphqlService\TheSimpleGraphqlService.csproj", "{618BA254-06F9-422A-A4B5-19973392D98F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {618BA254-06F9-422A-A4B5-19973392D98F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Debug|x64.ActiveCfg = Debug|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Debug|x64.Build.0 = Debug|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Debug|x86.ActiveCfg = Debug|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Debug|x86.Build.0 = Debug|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Release|Any CPU.Build.0 = Release|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Release|x64.ActiveCfg = Release|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Release|x64.Build.0 = Release|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Release|x86.ActiveCfg = Release|Any CPU + {618BA254-06F9-422A-A4B5-19973392D98F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/GlobalSuppressions.cs b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/GlobalSuppressions.cs new file mode 100644 index 00000000..26233fcb --- /dev/null +++ b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/GlobalSuppressions.cs @@ -0,0 +1 @@ +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "RECS0026:Possible unassigned object created by 'new'", Justification = "Constructs add themselves to the scope in which they are created")] diff --git a/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/Program.cs b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/Program.cs new file mode 100644 index 00000000..c2725d6c --- /dev/null +++ b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/Program.cs @@ -0,0 +1,17 @@ +using Amazon.CDK; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheSimpleGraphqlService +{ + sealed class Program + { + public static void Main(string[] args) + { + var app = new App(); + new TheSimpleGraphqlServiceStack(app, "TheSimpleGraphqlServiceStack"); + app.Synth(); + } + } +} diff --git a/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/TheSimpleGraphqlService.csproj b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/TheSimpleGraphqlService.csproj new file mode 100644 index 00000000..bcce7432 --- /dev/null +++ b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/TheSimpleGraphqlService.csproj @@ -0,0 +1,22 @@ + + + + Exe + netcoreapp3.1 + + Major + + + + + + + + + + + + + diff --git a/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/TheSimpleGraphqlServiceStack.cs b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/TheSimpleGraphqlServiceStack.cs new file mode 100644 index 00000000..48bb9e88 --- /dev/null +++ b/the-simple-graphql-service/csharp/src/TheSimpleGraphqlService/TheSimpleGraphqlServiceStack.cs @@ -0,0 +1,159 @@ +using Amazon.CDK; +using Lambda = Amazon.CDK.AWS.Lambda; +using DynamoDB = Amazon.CDK.AWS.DynamoDB; +using AppSync = Amazon.CDK.AWS.AppSync; + +namespace TheSimpleGraphqlService +{ + public class TheSimpleGraphqlServiceStack : Stack + { + + private readonly AppSync.GraphqlApi _graphqlApi; + private readonly AppSync.CfnApiKey _graphqlKey; + private readonly DynamoDB.Table _customerTable; + private readonly Lambda.Function _loyaltyLambda; + + internal TheSimpleGraphqlServiceStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + /** + * Create a new AppSync GraphQL API + */ + _graphqlApi = new AppSync.GraphqlApi(this, "Api", new AppSync.GraphqlApiProps + { + Name = "demoapi", + LogConfig = new AppSync.LogConfig + { + FieldLogLevel = AppSync.FieldLogLevel.ALL + }, + Schema = AppSync.Schema.FromAsset("./schema/schema.graphql") + }); + + /** + * Create Appsync Api Key + */ + _graphqlKey = new AppSync.CfnApiKey(this, "the-simple-graphql-service-api-key", new AppSync.CfnApiKeyProps + { + ApiId = _graphqlApi.ApiId + }); + + + /** + * Create new DynamoDB Table for Customer + */ + _customerTable = new DynamoDB.Table(this, "CustomerTable", new DynamoDB.TableProps + { + BillingMode = DynamoDB.BillingMode.PAY_PER_REQUEST, + PartitionKey = new DynamoDB.Attribute + { + Name = "id", + Type = DynamoDB.AttributeType.STRING + } + }); + + /** + * Add Customer DynamoDB as a Datasource for the Graphql API. + */ + var customerDS = _graphqlApi.AddDynamoDbDataSource("Customer", _customerTable); + + // Query Resolver to get all Customers + customerDS.CreateResolver(new AppSync.BaseResolverProps + { + TypeName = "Query", + FieldName = "getCustomers", + RequestMappingTemplate = AppSync.MappingTemplate.DynamoDbScanTable(), + ResponseMappingTemplate = AppSync.MappingTemplate.DynamoDbResultList() + }); + + // Query Resolver to get an individual Customer by their id + customerDS.CreateResolver(new AppSync.BaseResolverProps + { + TypeName = "Query", + FieldName = "getCustomer", + RequestMappingTemplate = AppSync.MappingTemplate.DynamoDbGetItem("id", "id"), + ResponseMappingTemplate = AppSync.MappingTemplate.DynamoDbResultItem() + }); + + // Mutation Resolver for adding a new Customer + customerDS.CreateResolver(new AppSync.BaseResolverProps + { + TypeName = "Mutation", + FieldName = "addCustomer", + RequestMappingTemplate = AppSync.MappingTemplate.DynamoDbPutItem( + AppSync.PrimaryKey.Partition("id").Auto(), + AppSync.Values.Projecting("customer") + ), + ResponseMappingTemplate = AppSync.MappingTemplate.DynamoDbResultItem() + }); + + + // Mutation Resolver for updating an exisiting Customer + customerDS.CreateResolver(new AppSync.BaseResolverProps + { + TypeName = "Mutation", + FieldName = "saveCustomer", + RequestMappingTemplate = AppSync.MappingTemplate.DynamoDbPutItem( + AppSync.PrimaryKey.Partition("id").Is("id"), + AppSync.Values.Projecting("customer") + ), + ResponseMappingTemplate = AppSync.MappingTemplate.DynamoDbResultItem() + }); + + + // Mutation resolver for creating a new customer along with their first order + customerDS.CreateResolver(new AppSync.BaseResolverProps + { + TypeName = "Mutation", + FieldName = "saveCustomerWithFirstOrder", + RequestMappingTemplate = AppSync.MappingTemplate.DynamoDbPutItem( + AppSync.PrimaryKey.Partition("order").Auto().Sort("customer").Is("customer.id"), + AppSync.Values.Projecting("order").Attribute("referral").Is("referral") + ), + ResponseMappingTemplate = AppSync.MappingTemplate.DynamoDbResultItem() + }); + + // Mutation Resolver for deleting an exisiting Customer + customerDS.CreateResolver(new AppSync.BaseResolverProps + { + TypeName = "Mutation", + FieldName = "removeCustomer", + RequestMappingTemplate = AppSync.MappingTemplate.DynamoDbDeleteItem("id", "id"), + ResponseMappingTemplate = AppSync.MappingTemplate.DynamoDbResultItem() + }); + + // defines an AWS Lambda resource + _loyaltyLambda = new Lambda.Function(this, "LoyaltyLambdaHandler", new Lambda.FunctionProps + { + Runtime = Lambda.Runtime.NODEJS_12_X, // execution environment + Code = Lambda.Code.FromAsset("lambda_fns"), // code loaded from the "lambda_fns" directory + Handler = "loyalty.handler" // file is "loyalty", function is "handler" + }); + + /** + * Add Loyalty Lambda as a Datasource for the Graphql API. + */ + var loyaltyDS = _graphqlApi.AddLambdaDataSource("Loyalty", _loyaltyLambda); + + // Query Resolver to get all Customers + loyaltyDS.CreateResolver(new AppSync.BaseResolverProps + { + TypeName = "Query", + FieldName = "getLoyaltyLevel", + RequestMappingTemplate = AppSync.MappingTemplate.LambdaRequest(), + ResponseMappingTemplate = AppSync.MappingTemplate.LambdaResult() + }); + + // GraphQL API Endpoint + new CfnOutput(this, "Endpoint", new CfnOutputProps + { + Value = _graphqlApi.GraphqlUrl + }); + + // API Key + new CfnOutput(this, "API_Key", new CfnOutputProps + { + Value = _graphqlApi.ApiKey + }); + + } + } +} diff --git a/the-simple-graphql-service/java/.gitignore b/the-simple-graphql-service/java/.gitignore new file mode 100644 index 00000000..1db21f16 --- /dev/null +++ b/the-simple-graphql-service/java/.gitignore @@ -0,0 +1,13 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml + +# CDK asset staging directory +.cdk.staging +cdk.out + diff --git a/the-simple-graphql-service/java/README.md b/the-simple-graphql-service/java/README.md new file mode 100644 index 00000000..b91bc05e --- /dev/null +++ b/the-simple-graphql-service/java/README.md @@ -0,0 +1,70 @@ +# The Simple GraphQL Service + +![architecture](../img/architecture.png) + +This is an example CDK stack to deploy The Simple [GraphQL](https://graphql.org/) Service inspired by [Thorsten Hoeger](https://twitter.com/hoegertn)'s contributions to the [CDK AppSync Module example]( https://docs.aws.amazon.com/cdk/api/latest/docs/aws-appsync-readme.html#usage-example) + +An advanced version of this pattern was talked about by [Heitor Lessa](https://twitter.com/heitor_lessa) at re:Invent 2019 as "The Cherry Pick". + +* [Youtube Recording](https://www.youtube.com/watch?v=9IYpGTS7Jy0) +* [Static Slides](https://d1.awsstatic.com/events/reinvent/2019/REPEAT_3_Serverless_architectural_patterns_and_best_practices_ARC307-R3.pdf) + +This is the most basic of [AppSync](https://aws.amazon.com/appsync/) implementations and would have to be hardened before production use. e.g. cognito user pools configured + +**Note, never print your API Key to the logs in a production system. This was added to make learning AppSync and GraphQL easier. If you want to use this pattern in a production system remove the two cloudformation outputs** + +### Testing + +1. After CDK Deploy, capture outputs from the log. + +```json +TheSimpleGraphqlServiceStack: deploying... +TheSimpleGraphqlServiceStack: creating CloudFormation changeset... + + TheSimpleGraphqlServiceStack + +Outputs: +TheSimpleGraphqlServiceStack.APIKey = +TheSimpleGraphqlServiceStack.Endpoint = https://.appsync-api.us-east-1.amazonaws.com/graphql +``` + +2. Setup Postman as outlined in Postman's [Using GraphQL Instructions](https://learning.postman.com/docs/postman/sending-api-requests/graphql/) + +3. Set POST request URL, x-api-key and Content-Type +* x-api-key = 'your API Key' +* Content-Type = application/graphql + +![postman](../img/postman-headers.png) + +4. Execute Mutations and Queries to exercice resolvers to dynamo and Lambda datasources + +![postman](../img/postman-queries.png) + +```json +// Inserts to Dynamo +mutation add { + addCustomer(customer: { name: "CDKPatterns"}) { + id + name + } +} + +// Queries the Dynamo DB +query getCustomers { + getCustomers{id name} +} + +// Executes the Lambda +query getLoyalty { + getLoyaltyLevel { level } +} +``` + +## Useful commands + + * `mvn package` compile and run tests + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation diff --git a/the-simple-graphql-service/java/cdk.json b/the-simple-graphql-service/java/cdk.json new file mode 100644 index 00000000..3b9f54cc --- /dev/null +++ b/the-simple-graphql-service/java/cdk.json @@ -0,0 +1,11 @@ +{ + "app": "mvn -e -q compile exec:java", + "context": { + "@aws-cdk/core:enableStackNameDuplicates": "true", + "aws-cdk:enableDiffNoFail": "true", + "@aws-cdk/core:stackRelativeExports": "true", + "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, + "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, + "@aws-cdk/aws-kms:defaultKeyPolicies": true + } +} diff --git a/the-simple-graphql-service/java/lambda_fns/loyalty.js b/the-simple-graphql-service/java/lambda_fns/loyalty.js new file mode 100644 index 00000000..4be794d1 --- /dev/null +++ b/the-simple-graphql-service/java/lambda_fns/loyalty.js @@ -0,0 +1,18 @@ +"use strict"; +const { Lambda } = require('aws-sdk'); +exports.handler = async function (event) { + console.log("get loyalty request:", JSON.stringify(event, undefined, 2)); + let loyaltylevel = "Silver"; + // return response back to upstream caller + return sendRes(200, loyaltylevel); +}; +const sendRes = (status, loyaltylevel) => { + var response = { + statusCode: status, + headers: { + "Content-Type": "application/json" + }, + level: loyaltylevel + }; + return response; +}; \ No newline at end of file diff --git a/the-simple-graphql-service/java/pom.xml b/the-simple-graphql-service/java/pom.xml new file mode 100644 index 00000000..9324e1fd --- /dev/null +++ b/the-simple-graphql-service/java/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + com.cdkpatterns + the-simple-graphql-service + 0.1 + + + UTF-8 + 1.83.0 + 5.7.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + com.cdkpatterns.TheSimpleGraphqlServiceApp + + + + + + + + + software.amazon.awscdk + core + ${cdk.version} + + + software.amazon.awscdk + lambda + ${cdk.version} + + + software.amazon.awscdk + appsync + ${cdk.version} + + + software.amazon.awscdk + dynamodb + ${cdk.version} + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.18.1 + test + + + diff --git a/the-simple-graphql-service/java/schema/schema.graphql b/the-simple-graphql-service/java/schema/schema.graphql new file mode 100644 index 00000000..ba36f7fc --- /dev/null +++ b/the-simple-graphql-service/java/schema/schema.graphql @@ -0,0 +1,35 @@ +type Customer { + id: String! + name: String! +} + +input SaveCustomerInput { + name: String! +} + +type Order { + customer: String! + order: String! +} + +type Loyalty { + level: String +} + +type Query { + getCustomers: [Customer] + getCustomer(id: String): Customer + getLoyaltyLevel(id: String): Loyalty +} + +input FirstOrderInput { + product: String! + quantity: Int! +} + +type Mutation { + addCustomer(customer: SaveCustomerInput!): Customer + saveCustomer(id: String!, customer: SaveCustomerInput!): Customer + removeCustomer(id: String!): Customer + saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order +} \ No newline at end of file diff --git a/the-simple-graphql-service/java/src/main/java/com/cdkpatterns/TheSimpleGraphqlServiceApp.java b/the-simple-graphql-service/java/src/main/java/com/cdkpatterns/TheSimpleGraphqlServiceApp.java new file mode 100644 index 00000000..b8dea84e --- /dev/null +++ b/the-simple-graphql-service/java/src/main/java/com/cdkpatterns/TheSimpleGraphqlServiceApp.java @@ -0,0 +1,15 @@ +package com.cdkpatterns; + +import software.amazon.awscdk.core.App; + +import java.util.Arrays; + +public class TheSimpleGraphqlServiceApp { + public static void main(final String[] args) { + App app = new App(); + + new TheSimpleGraphqlServiceStack(app, "TheSimpleGraphqlServiceStack"); + + app.synth(); + } +} diff --git a/the-simple-graphql-service/java/src/main/java/com/cdkpatterns/TheSimpleGraphqlServiceStack.java b/the-simple-graphql-service/java/src/main/java/com/cdkpatterns/TheSimpleGraphqlServiceStack.java new file mode 100644 index 00000000..2e414021 --- /dev/null +++ b/the-simple-graphql-service/java/src/main/java/com/cdkpatterns/TheSimpleGraphqlServiceStack.java @@ -0,0 +1,162 @@ +package com.cdkpatterns; + +import software.amazon.awscdk.core.CfnOutput; +import software.amazon.awscdk.core.Construct; +import software.amazon.awscdk.core.Stack; +import software.amazon.awscdk.core.StackProps; +import software.amazon.awscdk.services.appsync.GraphqlApi; +import software.amazon.awscdk.services.appsync.Schema; +import software.amazon.awscdk.services.appsync.DynamoDbDataSource; +import software.amazon.awscdk.services.appsync.LambdaDataSource; +import software.amazon.awscdk.services.appsync.CfnApiKey; +import software.amazon.awscdk.services.appsync.PrimaryKey; +import software.amazon.awscdk.services.appsync.Values; +import software.amazon.awscdk.services.appsync.MappingTemplate; +import software.amazon.awscdk.services.appsync.LogConfig; +import software.amazon.awscdk.services.appsync.FieldLogLevel; +import software.amazon.awscdk.services.appsync.BaseResolverProps; +import software.amazon.awscdk.services.dynamodb.Table; +import software.amazon.awscdk.services.dynamodb.BillingMode; +import software.amazon.awscdk.services.dynamodb.Attribute; +import software.amazon.awscdk.services.dynamodb.AttributeType; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.Runtime; + +public class TheSimpleGraphqlServiceStack extends Stack { + public TheSimpleGraphqlServiceStack(final Construct scope, final String id) { + this(scope, id, null); + } + + public TheSimpleGraphqlServiceStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + /* + * Create a new AppSync GraphQL API + */ + GraphqlApi grapqlApi = GraphqlApi.Builder.create(this, "Api") + .name("demoapi") + .logConfig(new LogConfig + .Builder() + .fieldLogLevel(FieldLogLevel.ALL) + .build()) + .schema(Schema.fromAsset("./schema/schema.graphql")) + .build(); + + /* + * Create Appsync Api Key + */ + CfnApiKey graphqlKey = CfnApiKey.Builder.create(this, "the-simple-graphql-service-api-key") + .apiId(grapqlApi.getApiId()) + .build(); + + /* + * Create new DynamoDB Table for Customer + */ + Table customerTable = Table.Builder.create(this, "CustomerTable") + .billingMode(BillingMode.PAY_PER_REQUEST) + .partitionKey(new Attribute + .Builder() + .name("id") + .type(AttributeType.STRING) + .build()) + .build(); + + /* + * Add Customer DynamoDB as a Datasource for the Graphql API. + */ + DynamoDbDataSource customerDS = grapqlApi.addDynamoDbDataSource("Customer", customerTable); + + // Query Resolver to get all Customers + customerDS.createResolver(new BaseResolverProps + .Builder() + .typeName("Query") + .fieldName("getCustomers") + .requestMappingTemplate(MappingTemplate.dynamoDbScanTable()) + .responseMappingTemplate(MappingTemplate.dynamoDbResultList()) + .build()); + + // Query Resolver to get an individual Customer by their id + customerDS.createResolver(new BaseResolverProps + .Builder() + .typeName("Query") + .fieldName("getCustomer") + .requestMappingTemplate(MappingTemplate.dynamoDbGetItem("id", "id")) + .responseMappingTemplate(MappingTemplate.dynamoDbResultItem()) + .build()); + + // Mutation Resolver for adding a new Customer + customerDS.createResolver(new BaseResolverProps + .Builder() + .typeName("Mutation") + .fieldName("addCustomer") + .requestMappingTemplate(MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition("id").auto(), + Values.projecting("customer"))) + .responseMappingTemplate(MappingTemplate.dynamoDbResultItem()) + .build()); + + // Mutation Resolver for updating an exisiting Customer + customerDS.createResolver(new BaseResolverProps + .Builder() + .typeName("Mutation") + .fieldName("saveCustomer") + .requestMappingTemplate(MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition("id").is("id"), + Values.projecting("customer"))) + .responseMappingTemplate(MappingTemplate.dynamoDbResultItem()) + .build()); + + // Mutation resolver for creating a new customer along with their first order + customerDS.createResolver(new BaseResolverProps + .Builder() + .typeName("Mutation") + .fieldName("saveCustomerWithFirstOrder") + .requestMappingTemplate(MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition("order").auto().sort("customer").is("customer.id"), + Values.projecting("order").attribute("referral").is("referral"))) + .responseMappingTemplate(MappingTemplate.dynamoDbResultItem()) + .build()); + + // Mutation Resolver for deleting an exisiting Customer + customerDS.createResolver(new BaseResolverProps + .Builder() + .typeName("Mutation") + .fieldName("removeCustomer") + .requestMappingTemplate(MappingTemplate.dynamoDbDeleteItem("id", "id")) + .responseMappingTemplate(MappingTemplate.dynamoDbResultItem()) + .build()); + + // defines an AWS Lambda resource + Function loyaltyLambda = Function.Builder.create(this, "LoyaltyLambdaHandler") + .runtime(Runtime.NODEJS_12_X) // execution environment + .code(Code.fromAsset("lambda_fns")) // code loaded from the "lambda_fns" directory + .handler("loyalty.handler") // code loaded from the "lambda_fns" directory + .build(); + + /* + * Add Loyalty Lambda as a Datasource for the Graphql API. + */ + LambdaDataSource loyaltyDS = grapqlApi.addLambdaDataSource("Loyalty", loyaltyLambda); + + // Query Resolver to get all Customers + loyaltyDS.createResolver(new BaseResolverProps + .Builder() + .typeName("Query") + .fieldName("getLoyaltyLevel") + .requestMappingTemplate(MappingTemplate.lambdaRequest()) + .responseMappingTemplate(MappingTemplate.lambdaResult()) + .build()); + + // GraphQL API Endpoint + CfnOutput.Builder.create(this, "Endpoint") + .value(grapqlApi.getGraphqlUrl()) + .build(); + + // API Key + CfnOutput.Builder.create(this, "API_Key") + .value(graphqlKey.getAttrApiKey()) + .build(); + + } +} diff --git a/the-simple-graphql-service/python/lambda_fns/loyalty.js b/the-simple-graphql-service/python/lambda_fns/loyalty.js index 1e1cd327..4be794d1 100644 --- a/the-simple-graphql-service/python/lambda_fns/loyalty.js +++ b/the-simple-graphql-service/python/lambda_fns/loyalty.js @@ -1,13 +1,12 @@ "use strict"; const { Lambda } = require('aws-sdk'); exports.handler = async function (event) { - console.log("request:", JSON.stringify(event, undefined, 2)); - - var loyaltylevel = "Silver" + console.log("get loyalty request:", JSON.stringify(event, undefined, 2)); + let loyaltylevel = "Silver"; // return response back to upstream caller return sendRes(200, loyaltylevel); }; -const sendRes = (status, body) => { +const sendRes = (status, loyaltylevel) => { var response = { statusCode: status, headers: { diff --git a/the-simple-graphql-service/python/the_simple_graphql_service/the_simple_graphql_service_stack.py b/the-simple-graphql-service/python/the_simple_graphql_service/the_simple_graphql_service_stack.py index 9502755e..7e0aae8b 100644 --- a/the-simple-graphql-service/python/the_simple_graphql_service/the_simple_graphql_service_stack.py +++ b/the-simple-graphql-service/python/the_simple_graphql_service/the_simple_graphql_service_stack.py @@ -20,12 +20,14 @@ def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: schema=appsync.Schema.from_asset(schema_location) ) + # Create Appsync Api Key api_key = appsync.CfnApiKey(self, 'the-simple-graphql-service-api-key', api_id=api.api_id ) # Create new DynamoDB Table for Customer customer_table = dynamo_db.Table(self, "CustomerTable", + billing_mode=dynamo_db.BillingMode.PAY_PER_REQUEST, partition_key=dynamo_db.Attribute(name="id", type=dynamo_db.AttributeType.STRING) ) diff --git a/the-simple-graphql-service/typescript/lib/the-simple-graphql-service-stack.ts b/the-simple-graphql-service/typescript/lib/the-simple-graphql-service-stack.ts index cab3f292..28ab612c 100644 --- a/the-simple-graphql-service/typescript/lib/the-simple-graphql-service-stack.ts +++ b/the-simple-graphql-service/typescript/lib/the-simple-graphql-service-stack.ts @@ -19,6 +19,9 @@ export class TheSimpleGraphQLServiceStack extends cdk.Stack { schema: new Schema({ filePath: join('__dirname', '/../', 'schema/schema.graphql') }), }); + /** + * Create Appsync Api Key + */ const apiKey = new CfnApiKey(this, 'the-simple-graphql-service-api-key', { apiId: api.apiId }); @@ -100,7 +103,7 @@ export class TheSimpleGraphQLServiceStack extends cdk.Stack { // defines an AWS Lambda resource const loyaltyLambda = new lambda.Function(this, 'LoyaltyLambdaHandler', { runtime: lambda.Runtime.NODEJS_12_X, // execution environment - code: lambda.Code.fromAsset('lambda-fns'), // code loaded from the "lambda" directory + code: lambda.Code.fromAsset('lambda-fns'), // code loaded from the "lambda-fns" directory handler: 'loyalty.handler', // file is "loyalty", function is "handler" });