С тех пор, как в ноябре 2022 года AWS объявила о поддержке преобразователей JavaScript для AWS AppSync, моя производительность и эффективность при создании API-интерфейсов GraphQL значительно повысились.

AWS AppSync предоставляет возможность создавать API-интерфейсы GraphQL, которые подключаются к различным источникам данных с помощью преобразователей. Эти преобразователи используются AWS AppSync для преобразования запросов и ответов GraphQL из различных источников данных.

Внедрение распознавателей JavaScript представляет собой альтернативу традиционному методу создания распознавателей AWS AppSync с помощью языка шаблонов Apache Velocity (VTL). Раньше для этого требовалось изучение нового языка, а тестировать резольверы VTL часто было сложно. Вот как может выглядеть извлечение элемента из таблицы DynamoDB с помощью VTL:

#set($tableName = "someTable")
#set($key = {
  "pk": $util.dynamodb.toDynamoDBJson($ctx.args.pk)
})

{
  "version": "2017-02-28",
  "operation": "GetItem",
  "key": $util.toJson($key),
  "tableName": $tableName
}

Многие разработчики, как и я, часто прибегали к написанию преобразователей VTL, которые вызывали функцию AWS Lambda, обычно также написанную на JavaScript. Однако это решение не было оптимальным, так как добавляло сложности, повышало производительность и повышало затраты, связанные с интеграцией AWS Lambda.

Резолверы JavaScript AppSync

Благодаря преобразователям JavaScript AppSync может взаимодействовать практически с любым сервисом AWS, имеющим источники данных, используя обычный JavaScript. Это включает интеграцию с DynamoDB, RDS, Lambda, OpenSearch, EventBridge и конечными точками HTTP. Более поздняя интеграция с конечными точками HTTP позволяет интегрироваться практически с любым доступным сервисом AWS.

Используя преобразователи JavaScript, разработка преобразователей намного проще, поскольку JavaScript — это язык, знакомый большинству разработчиков. Кроме того, использование Jest для тестирования упрощает процесс тестирования распознавателей. Давайте узнаем, как создавать и тестировать преобразователи JavaScript в AWS AppSync с помощью CDK!

Начиная

В этом примере мы продемонстрируем использование преобразователей JavaScript AWS AppSync с DynamoDB в качестве источника данных.

  1. Сначала мы создаем источник данных DynamoDB, который ссылается на наш AppSync GraphQL API и таблицу DynamoDB. Мы также определяем пользовательскую роль службы, чтобы обеспечить доступ AppSync к любым индексам в таблице.
const dynamoDbDataSource = new DynamoDbDataSource(this, `dynamoDbDataSource`, {
    api: api, // the AppSync GraphqlApi
    table: dataTable, // the DynamoDB Table
    serviceRole: new Role(this, 'DynamoDbAppSyncServiceRole', {
      assumedBy: new ServicePrincipal('appsync.amazonaws.com'),
      inlinePolicies: {
        name: new PolicyDocument({
          statements: [
            new PolicyStatement({
              effect: Effect.ALLOW,
              actions: [
                'dynamodb:BatchGetItem',
                'dynamodb:BatchWriteItem',
                'dynamodb:ConditionCheckItem',
                'dynamodb:DeleteItem',
                'dynamodb:DescribeTable',
                'dynamodb:GetItem',
                'dynamodb:GetRecords',
                'dynamodb:GetShardIterator',
                'dynamodb:PutItem',
                'dynamodb:Query',
                'dynamodb:Scan',
                'dynamodb:UpdateItem',
              ],
              // Note: '/*' to provide access to all indexes on the table
              resources: [dataTable.tableArn + '/*'],
            }),
          ],
        }),
      },
    }),
  });

⚠️Внимание⚠️

По умолчанию роль службы AppSync, созданная с помощью источника данных DynamoDB, имеет доступ только к основной таблице DynamoDB. Если в таблице есть какие-либо индексы (LSI/GSI), необходимо определить пользовательскую роль службы AppSync с соответствующими разрешениями.

2. Затем мы определяем нашу функцию AWS AppSync, содержащую код преобразователя JavaScript. Имя функции совпадает с именем действия в нашей схеме GraphQL.

const getItemFunction = new AppsyncFunction(this, 'getItem', {
  name: 'getItem',
  api: api, // the AppSync GraphqlApi
  dataSource: dynamoDbDataSource, // the Data Source in step 1
  code: Code.fromAsset(path.join(__dirname, '/graphql/Query.getItem.js')),
  runtime: FunctionRuntime.JS_1_0_0,
});

3. Код JavaScript для самого распознавателя довольно прост. Здесь мы можем запросить нашу таблицу DynamoDB с помощью ключа раздела.

// Query.getItem.js

import { util } from '@aws-appsync/utils';

export function request(ctx) {
  return {
    version: '2017-02-28',
    operation: 'Query',
    query: {
      expression: 'pk = :pk',
      expressionValues: {
        ':pk': util.dynamodb.toDynamoDB(ctx.args.pk),
      },
    },
  };
}

export function response(ctx) {
  return ctx.result;
}

⚠️Внимание⚠️

При использовании других операций DynamoDB, таких как TransactionWriteItem, имя таблицы обязательно. Если имя таблицы отличается для каждой среды приложения, в настоящее время нет возможности предоставить динамическое значение распознавателю. Обходной путь заключается в том, чтобы написать встроенный код преобразователя в стек CDK или предоставить имя таблицы как часть контекста запроса GraphQL. Надеемся, что AWS AppSync предоставит решение этой проблемы в ближайшем будущем. Ниже приведен один из примеров такой ситуации:

export function request(ctx) {
  return {
    operation: 'TransactWriteItems',
    transactItems: [
    {
      // table name passed in via request conext
      table: `my-${ctx.args.input.envName}-table`,
      operation: 'PutItem',

      // Rest of code omitted for brevity

4. Поскольку резолверы AppSync JavaScript поддерживают только резолверы конвейера, на данный момент нам нужно включить код для поддержки передачи контекстов между каждым резолвером (даже если есть только один резолвер).

const passthrough = InlineCode.fromInline(`
    // The before step
    export function request(...args) {
      return {}
    }

    // The after step
    export function response(ctx) {
      return ctx.prev.result
    }
`);

5. Наконец, мы определяем наш преобразователь, который содержит единственную функцию, определенную ранее. Как отмечалось ранее, мы должны создать преобразователь конвейера при использовании JavaScript, поэтому обратите внимание, что pipeConfig содержит только наш единственный преобразователь. Если бы возникла необходимость связать несколько действий вместе, сюда можно было бы включить дополнительные распознаватели.

const getItemResolver = new Resolver(this, 'getItemResolver', {
  api: api, // the AppSync GrapqlApi
  typeName: 'Query',
  fieldName: 'getItem',
  runtime: FunctionRuntime.JS_1_0_0,
  pipelineConfig: [getItemFunction], // the AppSync Function in step 2
  code: passthrough, // the pipeline in step 3
});

После этого у нас есть все необходимое, чтобы начать использовать нашу конечную точку AWS AppSync GraphQL с распознавателем JavaScript.

Тестирование

Традиционно тестирование резолверов VTL требовало некоторой работы, но с использованием JavaScript тестирование можно легко выполнить с помощью известных библиотек тестирования JavaScript, таких как Jest.

Для тестирования распознавателя мы используем команду EvaluateCode из AWS SDK v3, которая вызывает AWS AppSync и оценивает код с предоставленным контекстом. Мы можем быстро проверить значения ответов в конечной точке GraphQL.

import { AppSyncClient, EvaluateCodeCommand, EvaluateCodeCommandInput } from '@aws-sdk/client-appsync';
import { unmarshall } from '@aws-sdk/util-dynamodb';
import { readFile } from 'fs/promises';
const appsync = new AppSyncClient({ region: 'us-east-1' });
const file = './lib/graphql/Query.getItem.js';

describe('getItem', () => {
  it('returns and item from the partition key', async () => {
    // Arrange
    const context = {
      arguments: {
        pk: '123',
      },
    };
    const input: EvaluateCodeCommandInput = {
      runtime: { name: 'APPSYNC_JS', runtimeVersion: '1.0.0' },
      code: await readFile(file, { encoding: 'utf8' }),
      context: JSON.stringify(context),
      function: 'request',
    };
    const evaluateCodeCommand = new EvaluateCodeCommand(input);
  
    // Act
    const response = await appsync.send(evaluateCodeCommand);
  
    // Assert
    expect(response).toBeDefined();
    expect(response.error).toBeUndefined();
    expect(response.evaluationResult).toBeDefined();
  
    const result = JSON.parse(response.evaluationResult ?? '{}');
    expect(result.operation).toEqual('Query');
    expect(result.query.expression).toEqual('pk = :pk');
  
    const expressionValues = unmarshall(result.query.expressionValues);
    expect(expressionValues[':pk']).toEqual(context.arguments.pk);
  });
});

Преимущества преобразователей JavaScript

Внедрение распознавателей JavaScript принесло много важных улучшений в рабочий процесс разработки наряду с некоторыми ограничениями.

  • Улучшенный опыт разработчика — за счет предоставления хорошо известного, знакомого опыта разработки на JavaScript, дополненного поддержкой IDE и анализом кода с помощью @aws-appsync/eslint-plugin.
  • Простота тестирования — за счет использования тех же библиотек JavaScript, которые существуют сегодня, например, Jest.
  • Поддержка набора текста — с помощью TypeScript, при условии, что код транспилируется в JavaScript перед развертыванием.
  • Чистый код — разработка повторно используемого кода или пользовательских пакетов в большой экосистеме JavaScript.
  • Больше никаких холодных запусков — это прямая интеграция с источником данных, не требующая функций Lambda (за исключением Lambda Provisioned Concurrency)
  • Бесплатно — встроено в AWS AppSync, поэтому не требует дополнительных затрат.

Ограничения распознавателей JavaScript

  • Неподдерживаемые функции JavaScript — при использовании APPSYNC_JS runtime (ограниченное подмножество ECMAScript 6) многие функции JavaScript, такие как циклы, try-catch, генерация ошибок, стрелочные функции, async/await, не поддерживаются. . Подробности смотрите в Рабочей документации.
  • Нет переменных среды — нет возможности передавать переменные среды в преобразователь JavaScript, такой как AWS Lambda. Например, при использовании DynamoDB в качестве источника данных может потребоваться указать имя таблицы, которое может быть разным для каждой среды приложения.
  • Ограниченная поддержка пакетов — невозможность импортировать любой доступный пакет npm, который не совместим со средой выполнения APPSYNC_JS или создает пакет размером более 32 КБ. Возможно, это было бы отличной возможностью для создания пользовательских библиотек, совместимых с appsync?
  • Только конвейерные преобразователи — чтобы один преобразователь Unit использовал VTL или создавал конвейерный преобразователь с одной функцией (как в нашем примере).

Подводя итоги

Более подробное сравнение резолверов JavaScript AWS AppSync и Lambda в качестве источника данных см. в разделе Сравнение AWS.

Поддержка преобразователей JavaScript в AWS AppSync значительно упростила разработку API GraphQL. Если вам понравилась эта статья, в следующей серии мы рассмотрим, как использовать источник данных конечной точки HTTP, а также прямую интеграцию с EventBridge.

Эрик Бах — старший разработчик программного обеспечения в Alberta Motor Association, которому нравится учиться, читать и писать о принципах лидерства, управляемых событиями микросервисах и обо всем, что связано с AWS.