服务
关于
CloudProse博客
聚光灯

AppSync与AWS云开发套件

来自Amplify CLI的GraphQL SDL的原始功能以及CDK的无与伦比的灵活性。
肯·赢家
肯·温纳 | 2020年4月16日

注意:可以找到所有代码和信息 在GitHub上 。这是Trek10前团队成员的客座帖子,现为CTO GetVoxi ,。非常感谢!

我找到了一种将AWS生态系统提供的各种工具的某些最佳部分融合到一些我认为其中一些人可能会喜欢学习的东西上的方法。我们将从技术开始,然后转向我们所做的工作。

我们喜欢AppSync

由于一些关键点,我们选择AppSync作为基础。 GraphQL是在客户端之上构建客户端的出色界面,AppSync是针对各种数据存储(例如DynamoDB)实施GraphQL服务的“ AWS本机”方式。此外,您还可以获得一些简洁的功能,例如 订阅。我们发现以AppSync作为API层进行构建可以为项目带来便利和可预测的结果。

AppSync面临的挑战

如果您熟悉AppSync,就会知道构建完整的API,为模型,连接,过滤器,查询和变异编写模式的每个部分会多么令人沮丧。您不仅必须编写架构,还必须使用速度模板语言(VTL)编写每个解析器。一个简单的应用程序可以迅速成为数百行SDL,VTL和Cloudformation。

GraphQL转换 节省一天

Amplify CLI引入了一些出色的程序包,以使用GraphQL架构定义语言(SDL)将AppSync架构转换为类型,查询,突变,订阅,表和解析器。使用 支持的指令 CLI转换插件会将您的SDL转换为可部署的模板,从而简化了创建AppSync API的过程。它将几乎所有必需的请求和响应VTL模板以及数据存储区设置并连接在一起,从而使原本相当费力的工作耗时数分钟。

An example directive for the @model directive looks like this:

type Product
  @model {
    id: ID!
    name: String!
    description: String!
    price: String!
    active: Boolean!
    added: AWSDateTime!
}

转换后,我们将获得以下架构以及DynamoDB表的解析器和CloudFormation。

type Product {
  id: ID!
  name: String!
  description: String!
  price: String!
  active: Boolean!
  added: AWSDateTime!
}

type ModelProductConnection {
  items: [Product]
  nextToken: String
}

input CreateProductInput {
  id: ID
  name: String!
  description: String!
  price: String!
  active: Boolean!
  added: AWSDateTime!
}

input UpdateProductInput {
  id: ID!
  name: String
  description: String
  price: String
  active: Boolean
  added: AWSDateTime
}

input DeleteProductInput {
  id: ID
}

input ModelProductFilterInput {
  id: ModelIDFilterInput
  name: ModelStringFilterInput
  description: ModelStringFilterInput
  price: ModelStringFilterInput
  active: ModelBooleanFilterInput
  added: ModelStringFilterInput
  and: [ModelProductFilterInput]
  or: [ModelProductFilterInput]
  not: ModelProductFilterInput
}

type Query {
  getProduct(id: ID!): Product
  listProducts(filter: ModelProductFilterInput, limit: Int, nextToken: String): ModelProductConnection
}

type Mutation {
  createProduct(input: CreateProductInput!): Product
  updateProduct(input: UpdateProductInput!): Product
  deleteProduct(input: DeleteProductInput!): Product
}

type Subscription {
  onCreateProduct: Product @aws_subscribe(mutations: ["createProduct"])
  onUpdateProduct: Product @aws_subscribe(mutations: ["updateProduct"])
  onDeleteProduct: Product @aws_subscribe(mutations: ["deleteProduct"])
}

使用GraphQL Transform插件,我们将带有声明的9行SDL转换为62行。将其推断为多种类型,我们开始了解自动转换如何不仅节省我们的时间,而且还为我们提供了一种简单的方式来声明围绕AppSync API的样板。

使用AWS Amplify CLI的挑战

尽管Amplify CLI的许多功能都非常出色,但我发现我个人更喜欢使用Amplify CLI定义资源。 AWS云开发套件 (CDK),因为它更容易与其他现有系统和流程集成。对我来说不幸的是,转换插件仅存在于Amplify CLI中。我决定要模拟此功能,我将使用Amplify CLI中使用的相同转换包,并将其集成到我的CDK项目中!

在进一步说明之前,我确实要指出,Amplify具有可扩展性和内置的“逃生舱口”,以便人们在不需要所有组件时可以根据需要使用零件。实际上,本文剩下的部分我们将依靠 是有记录的做法!

重新创建架构转换器

为了模拟Amplify CLI转换器,我们必须有一个模式转换器并导入现有的转换器。幸运的是,Amplify文档向我们展示了一个实现 这里 。由于我们希望所有相同的指令都可用,因此我们必须实现上述相同的程序包和结构。这为我们提供了指令解析,解析器创建和模板生成!

我们最终得到这样的结果:

import { GraphQLTransform } from 'graphql-transformer-core';
import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer';
import { ModelConnectionTransformer } from 'graphql-connection-transformer';
import { KeyTransformer } from 'graphql-key-transformer';
import { FunctionTransformer } from 'graphql-function-transformer';
import { VersionedModelTransformer } from 'graphql-versioned-transformer';
import { ModelAuthTransformer, ModelAuthTransformerConfig } from 'graphql-auth-transformer'
const { AppSyncTransformer } = require('graphql-appsync-transformer')
import { normalize } from 'path';
import * as fs from "fs";

const outputPath = './appsync'

export class SchemaTransformer {
    transform() {
        // These config values do not even matter... So set it up for both
        const authTransformerConfig: ModelAuthTransformerConfig = {
            authConfig: {
                defaultAuthentication: {
                    authenticationType: 'API_KEY',
                    apiKeyConfig: {
                        description: 'Testing',
                        apiKeyExpirationDays: 100
                    }
                },
                additionalAuthenticationProviders: [
                    {
                        authenticationType: 'AMAZON_COGNITO_USER_POOLS',
                        userPoolConfig: {
                            userPoolId: '12345xyz'
                        }
                    }
                ]
            }
        }

        // Note: This is not exact as we are omitting the @searchable 变压器.
        const 变压器 = new GraphQLTransform({
            变压器s: [
                new AppSyncTransformer(outputPath),
                new DynamoDBModelTransformer(),
                new VersionedModelTransformer(),
                new FunctionTransformer(),
                new KeyTransformer(),
                new ModelAuthTransformer(authTransformerConfig),
                new ModelConnectionTransformer(),
            ]
        })

        const schema_path = './schema.graphql'
        const schema = fs.readFileSync(schema_path)

        return 变压器.transform(schema.toString());
    }
}

编写我们自己的变压器

在实现了相同的模式转换器之后,我意识到它并不完全适合我们的CDK实现。例如,我们希望可以通过CDK创建的可迭代资源,而不是DynamoDB表的JSON CloudFormation输出。我们自己来 变压器!

在此自定义转换器中,我们做两件事-查找@nullable指令,并在完成后获取转换器上下文。

@nullable指令

When creating a custom key using the @key directive on a model, the associated resolver does not 所有 ow for using $util.autoId() to 生成唯一的标识符和创建时间. There are a couple of existing options, but we wanted to provide a "consistent" behavior to our developers that was easy to implement, so I created the "nullable" directive to enable using a custom @key directive that would autoId the id field if it wasn't passed in.

# We use my nullable tag so that the create can have an autoid on the ID field
type Order
    @model
    @key(fields: ["id", "productID"]) {
        id: ID! @nullable
        productID: ID!
        total: String!
        ordered: AWSDateTime!
}

我已经使用 graphql自动变压器 作为指导。这会使用我们的自定义指令为该字段输出一个经过修改的解析器。

后期转换

After schema transformation is complete, our custom 变压器 grabs the context, searches for AWS::DynamoDB::Table resources, and builds a table object for us to create a table from later. Later, we can loop over this output and create our tables and resolvers like so:

createTablesAndResolvers(api: GraphQLApi, tableData: any, resolvers: any) {
    Object.keys(tableData).forEach((tableKey: any) => {
      let table = this.createTable(tableData[tableKey]);

      const dataSource = api.addDynamoDbDataSource(tableKey, `Data source for ${tableKey}`, table);

      Object.keys(resolvers).forEach((resolverKey: any) => {
        let resolverTableName = this.getTableNameFromFieldName(resolverKey)
        if (tableKey === resolverTableName) {
          let resolver = resolvers[resolverKey]

          dataSource.createResolver({
            typeName: resolver.typeName,
            fieldName: resolver.fieldName,
            requestMappingTemplate: MappingTemplate.fromFile(resolver.requestMappingTemplate),
            responseMappingTemplate: MappingTemplate.fromFile(resolver.responseMappingTemplate),
          })
        }
      })
    });
  }

使用架构转换器

要在生成CDK模板之前运行我们的转换器,我们必须导入我们的转换器,运行该转换器,并将数据传递给我们的堆栈!

#!/usr/bin/env node
import * as cdk from '@aws-cdk/core';
import { AppStack } from '../lib/app-stack';
import { SchemaTransformer } from '../lib/schema-transformer';

const 变压器 = new SchemaTransformer();
const outputs = 变压器.transform();
const resolvers = 变压器.getResolvers();

const STAGE = process.env.STAGE || 'demo'

const app = new cdk.App({ 
    context: { STAGE: STAGE }
})

new AppStack(app, 'AppStack', outputs, resolvers);

可以找到所有代码 在Github上 。重要的是要注意是否要采用这种方法,请确保固定变压器的版本并在将来进行验证。由于我们是从现有的Amplify CLI借用的,因此没有既定的合同,Amplify可能不会随着事情的发展而改变。

我们从这里去哪里?

我们认为,将其作为CDK插件或npm软件包会更好。不幸的是,目前CDK插件系统仅支持凭据提供程序。我曾尝试将其作为插件编写(它的工作原理),但是您必须将cfdoc写入文件并从应用程序中读取它才能引入资源。

参考文献

作者
肯·赢家
肯·温纳

Ken拥有从AWS到Azure的云服务以及桌面应用程序的背景知识。他目前是GetVoxi的CTO。