Uso de resolvedores do AWS Lambda no AWS AppSync
Você pode usar AWS Lambda com o AWS AppSync para resolver qualquer campo do GraphQL. Por exemplo, uma consulta do GraphQL pode enviar uma chamada para uma instância do Amazon Relational Database Service (Amazon RDS), e uma mutação do GraphQL pode ser gravada em um stream do Amazon Kinesis. Nesta seção, mostraremos como escrever uma função do Lambda que executa lógica de negócios com base na invocação de uma operação de campo do GraphQL.
Powertools para AWS Lambda
O manipulador de eventos do Powertools for AWS Lambda GraphQL simplifica o roteamento e o processamento de eventos GraphQL em funções do Lambda. Ele está disponível para Python e Typescript. Leia mais sobre o manipulador de eventos da API GraphQL no Powertools para obter a documentação do AWS Lambda, consulte as referências a seguir.
Criar uma função do Lambda
O exemplo a seguir mostra uma função do Lambda escrita em Node.js(runtime: Node.js 18.x) que executa diferentes operações em publicações de blog como parte de um aplicativo de publicações de blog. Observe que o código deve ser salvo em um nome de arquivo com extensão .mis.
export const handler = async (event) => { console.log('Received event {}', JSON.stringify(event, 3)) const posts = { 1: { id: '1', title: 'First book', author: 'Author1', url: 'https://amazon.com/', content: 'SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1', ups: '100', downs: '10', }, 2: { id: '2', title: 'Second book', author: 'Author2', url: 'https://amazon.com', content: 'SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT', ups: '100', downs: '10', }, 3: { id: '3', title: 'Third book', author: 'Author3', url: null, content: null, ups: null, downs: null }, 4: { id: '4', title: 'Fourth book', author: 'Author4', url: 'https://www.amazon.com/', content: 'SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4', ups: '1000', downs: '0', }, 5: { id: '5', title: 'Fifth book', author: 'Author5', url: 'https://www.amazon.com/', content: 'SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT', ups: '50', downs: '0', }, } const relatedPosts = { 1: [posts['4']], 2: [posts['3'], posts['5']], 3: [posts['2'], posts['1']], 4: [posts['2'], posts['1']], 5: [], } console.log('Got an Invoke Request.') let result switch (event.field) { case 'getPost': return posts[event.arguments.id] case 'allPosts': return Object.values(posts) case 'addPost': // return the arguments back return event.arguments case 'addPostErrorWithData': result = posts[event.arguments.id] // attached additional error information to the post result.errorMessage = 'Error with the mutation, data has changed' result.errorType = 'MUTATION_ERROR' return result case 'relatedPosts': return relatedPosts[event.source.id] default: throw new Error('Unknown field, unable to resolve ' + event.field) } }
Essa função do Lambda recupera uma publicação por ID, adiciona uma publicação, recupera uma lista de publicações e busca publicações relacionadas para determinada publicação.
nota
A função do Lambda usa a instrução switch em event.field para determinar qual campo está sendo resolvido no momento.
Crie essa função do Lambda usando o console de gerenciamento da AWS.
Configurar a fonte de dados para o Lambda
Depois de criar a função do Lambda, navegue até a API GraphQL no console AWS AppSync e escolha a guia Fontes de dados.
Escolha Criar fonte de dados, insira um Nome de fonte de dados fácil de usar (por exemplo, Lambda) e, em seguida, para Tipo de fonte de dados, escolha Função AWS Lambda. Em Região, escolha a mesma região da sua função. Para ARN da função, escolha o nome do recurso da Amazon (ARN) da sua função do Lambda.
Depois de selecionar a função do Lambda, você pode criar um perfil do IAM AWS Identity and Access Management (para a qual o AWS AppSync atribui as permissões adequadas) ou selecionar uma função existente que possui a seguinte política em linha:
Você também deve configurar uma relação de confiança com o AWS AppSync para o perfil do IAM da seguinte maneira:
Criar um esquema do GraphQL
Agora que a fonte de dados está conectada à função do Lambda, crie um esquema do GraphQL.
No editor de esquemas no console do AWS AppSync, verifique se seu esquema corresponde ao esquema a seguir:
schema { query: Query mutation: Mutation } type Query { getPost(id:ID!): Post allPosts: [Post] } type Mutation { addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int relatedPosts: [Post] }
Configurar resolvedores
Agora que você registrou uma fonte de dados do Lambda e um esquema do GraphQL válido, pode conectar seus campos do GraphQL à sua fonte de dados do Lambda usando resolvedores.
Você criará um resolvedor que usa o runtime JavaScript (APPSYNC_JS) do AWS AppSync e interage com suas funções do Lambda. Para saber mais sobre como escrever resolvedores e funções do AWS AppSync com JavaScript, consulte Atributos de runtime de JavaScript para resolvedores e funções.
Para obter mais informações sobre modelos de mapeamento do Lambda, consulte Referência de função do resolvedor de JavaScript para o Lambda.
Nesta etapa, você anexa um resolvedor à função do Lambda para os seguintes campos: getPost(id:ID!):
Post, allPosts: [Post], addPost(id: ID!, author: String!, title: String,
content: String, url: String): Post! e Post.relatedPosts: [Post]. No editor Esquema do console do AWS AppSync, no painel Resolvedores, escolha Anexar ao lado do campo getPost(id:ID!): Post. Escolha sua fonte de dados do Lambda. Depois, forneça o seguinte código:
import { util } from '@aws-appsync/utils'; export function request(ctx) { const {source, args} = ctx return { operation: 'Invoke', payload: { field: ctx.info.fieldName, arguments: args, source }, }; } export function response(ctx) { return ctx.result; }
Esse código do resolvedor passa o nome do campo, a lista de argumentos e o contexto sobre o objeto de origem para a função do Lambda quando ela o invoca. Escolha Salvar.
Você anexou seu primeiro resolvedor com sucesso. Repita essa operação para os campos restantes.
Teste da sua API GraphQL
Agora que a função do Lambda está conectada aos resolvedores do GraphQL, você pode executar algumas mutações e consultas usando o console ou um aplicativo cliente.
No lado esquerdo do console do AWS AppSync, escolha Consultas e cole o seguinte código:
Mutação addPost
mutation AddPost { addPost( id: 6 author: "Author6" title: "Sixth book" url: "https://www.amazon.com/" content: "This is the book is a tutorial for using GraphQL with AWS AppSync." ) { id author title content url ups downs } }
Consulta getPost
query GetPost { getPost(id: "2") { id author title content url ups downs } }
Consulta allPosts
query AllPosts { allPosts { id author title content url ups downs relatedPosts { id title } } }
Retornar erros
Qualquer resolução de campo determinada pode resultar em um erro. Com o AWS AppSync, é possível gerar erros nas seguintes fontes:
-
Manipulador de respostas do resolvedor
-
Função do Lambda
No manipulador de respostas do resolvedor
Para gerar erros intencionais, você pode usar o método utilitário util.error. Ele utiliza como argumento uma errorMessage, um errorType e um valor data opcional. O data é útil para retornar dados adicionais de volta ao cliente quando ocorre um erro. O objeto data é adicionado ao errors na resposta final do GraphQL.
O exemplo a seguir mostra como usá-lo no manipulador de respostas do resolvedor Post.relatedPosts: [Post].
// the Post.relatedPosts response handler export function response(ctx) { util.error("Failed to fetch relatedPosts", "LambdaFailure", ctx.result) return ctx.result; }
Isso produz uma resposta do GraphQL semelhante à seguinte:
{ "data": { "allPosts": [ { "id": "2", "title": "Second book", "relatedPosts": null }, ... ] }, "errors": [ { "path": [ "allPosts", 0, "relatedPosts" ], "errorType": "LambdaFailure", "locations": [ { "line": 5, "column": 5 } ], "message": "Failed to fetch relatedPosts", "data": [ { "id": "2", "title": "Second book" }, { "id": "1", "title": "First book" } ] } ] }
Em que allPosts[0].relatedPosts é nulo, pois o erro e o errorMessage, o errorType e o data estão presentes no objeto data.errors[0].
A partir da função do Lambda
O AWS AppSync também entende os erros gerados pela função do Lambda. O modelo de programação do Lambda permite gerar erros processados. Se a função do Lambda gerar um erro, o AWS AppSync não conseguirá resolver o campo atual. Somente a mensagem de erro retornada a partir do Lambda é definida na resposta. No momento, não é possível enviar quaisquer dados adicionais de volta para o cliente ao gerar um erro a partir da função do Lambda.
nota
Se sua função do Lambda gerar um erro não tratado, o AWS AppSync usará a mensagem de erro definida pelo Lambda.
A seguinte função do Lambda gera um erro:
export const handler = async (event) => { console.log('Received event {}', JSON.stringify(event, 3)) throw new Error('I always fail.') }
O erro é recebido em seu manipulador de respostas. Você pode enviá-lo de volta na resposta do GraphQL anexando o erro à resposta com util.appendError. Para isso, altere o manipulador de respostas da função do AWS AppSync:
// the lambdaInvoke response handler export function response(ctx) { const { error, result } = ctx; if (error) { util.appendError(error.message, error.type, result); } return result; }
Isso retorna uma resposta do GraphQL semelhante à seguinte:
{ "data": { "allPosts": null }, "errors": [ { "path": [ "allPosts" ], "data": null, "errorType": "Lambda:Unhandled", "errorInfo": null, "locations": [ { "line": 2, "column": 3, "sourceName": null } ], "message": "I fail. always" } ] }
Caso de uso avançado: agrupamento em lotes
Neste exemplo, a função do Lambda tem um campo relatedPosts que retorna uma lista de publicações relacionadas para uma publicação. Nas consultas de exemplo, a invocação do campo allPosts a partir da função do Lambda retorna cinco publicações. Como também especificamos que queremos resolver relatedPosts para cada publicação retornada, a operação do campo relatedPosts será invocada cinco vezes.
query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yields 5 posts id title } } }
Embora isso possa não parecer substancial neste exemplo específico, essa busca excessiva combinada pode prejudicar rapidamente o aplicativo.
Se você buscasse relatedPosts novamente nas Posts relacionadas retornadas na mesma consulta, o número de invocações aumentaria drasticamente.
query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yield 5 posts = 5 x 5 Posts id title relatedPosts { // 5 x 5 Lambda invocations - each yield 5 posts = 25 x 5 Posts id title author } } } }
Nessa consulta relativamente simples, o AWS AppSync invocaria a função do Lambda 1 + 5 + 25 = 31 vezes.
Esse é um desafio bastante comum e normalmente é chamado de problema N+1 (nesse caso, N = 5) e pode incorrer em maior latência e mais custo para o aplicativo.
Uma forma de resolver esse problema é agrupar solicitações do resolvedor de campo semelhantes em lotes. Nesse exemplo, em vez da função do Lambda resolver uma lista de publicações relacionadas para uma única publicação, ela resolveria uma lista de publicações relacionadas para um determinado lote de publicações.
Para demonstrar isso, vamos atualizar o resolvedor para relatedPosts lidar com lotes.
import { util } from '@aws-appsync/utils'; export function request(ctx) { const {source, args} = ctx return { operation: ctx.info.fieldName === 'relatedPosts' ? 'BatchInvoke' : 'Invoke', payload: { field: ctx.info.fieldName, arguments: args, source }, }; } export function response(ctx) { const { error, result } = ctx; if (error) { util.appendError(error.message, error.type, result); } return result; }
O código agora altera a operação de Invoke para BatchInvoke quando fieldName está sendo resolvido é relatedPosts. Agora, habilite o lote na função na seção Configurar lotes. Defina o tamanho máximo do lote definido como 5. Escolha Salvar.
Com essa alteração, ao resolver relatedPosts, a função do Lambda recebe como entrada o seguinte:
[ { "field": "relatedPosts", "source": { "id": 1 } }, { "field": "relatedPosts", "source": { "id": 2 } }, ... ]
Quando BatchInvoke for especificado na solicitação, a função do Lambda recebe uma lista de solicitações e retorna uma lista de resultados.
Especificamente, a lista de resultados deve corresponder em tamanho e ordem das entradas de carga da solicitação para que o AWS AppSync possa combinar os resultados de acordo.
Neste exemplo de processamento em lotes, a função do Lambda retorna um lote de resultados da seguinte forma:
[ [{"id":"2","title":"Second book"}, {"id":"3","title":"Third book"}], // relatedPosts for id=1 [{"id":"3","title":"Third book"}] // relatedPosts for id=2 ]
Você pode atualizar seu código do Lambda para lidar com lotes para relatedPosts:
export const handler = async (event) => { console.log('Received event {}', JSON.stringify(event, 3)) //throw new Error('I fail. always') const posts = { 1: { id: '1', title: 'First book', author: 'Author1', url: 'https://amazon.com/', content: 'SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1', ups: '100', downs: '10', }, 2: { id: '2', title: 'Second book', author: 'Author2', url: 'https://amazon.com', content: 'SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT', ups: '100', downs: '10', }, 3: { id: '3', title: 'Third book', author: 'Author3', url: null, content: null, ups: null, downs: null }, 4: { id: '4', title: 'Fourth book', author: 'Author4', url: 'https://www.amazon.com/', content: 'SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4', ups: '1000', downs: '0', }, 5: { id: '5', title: 'Fifth book', author: 'Author5', url: 'https://www.amazon.com/', content: 'SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT', ups: '50', downs: '0', }, } const relatedPosts = { 1: [posts['4']], 2: [posts['3'], posts['5']], 3: [posts['2'], posts['1']], 4: [posts['2'], posts['1']], 5: [], } if (!event.field && event.length){ console.log(`Got a BatchInvoke Request. The payload has ${event.length} items to resolve.`); return event.map(e => relatedPosts[e.source.id]) } console.log('Got an Invoke Request.') let result switch (event.field) { case 'getPost': return posts[event.arguments.id] case 'allPosts': return Object.values(posts) case 'addPost': // return the arguments back return event.arguments case 'addPostErrorWithData': result = posts[event.arguments.id] // attached additional error information to the post result.errorMessage = 'Error with the mutation, data has changed' result.errorType = 'MUTATION_ERROR' return result case 'relatedPosts': return relatedPosts[event.source.id] default: throw new Error('Unknown field, unable to resolve ' + event.field) } }
Retornar erros individuais
Os exemplos anteriores mostram que é possível retornar um único erro da função do Lambda ou gerar um erro do seu manipulador de respostas. Para invocações em lote, gerar um erro a partir da função do Lambda sinaliza um lote inteiro como falho. Isso pode ser aceitável em cenários específicos onde ocorreu um erro irrecuperável, como uma conexão com falha a um armazenamento de dados. No entanto, nos casos em que alguns itens no lote são bem-sucedidos e outros falham, é possível retornar ambos os erros e os dados válidos. Como o AWS AppSync exige que a resposta do lote liste os elementos que correspondem ao tamanho original do lote, você deve definir uma estrutura de dados que possa diferenciar dados válidos de um erro.
Por exemplo, se houver expectativa da função do Lambda retornar um lote de publicações relacionadas, você poderá optar por retornar uma lista de objetos Response em que cada objeto tem campos data, errorMessage e errorType opcionais. Se o campo errorMessage estiver presente, isso significa que ocorreu um erro.
O código a seguir mostra como você pode atualizar a função do Lambda:
export const handler = async (event) => { console.log('Received event {}', JSON.stringify(event, 3)) // throw new Error('I fail. always') const posts = { 1: { id: '1', title: 'First book', author: 'Author1', url: 'https://amazon.com/', content: 'SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1', ups: '100', downs: '10', }, 2: { id: '2', title: 'Second book', author: 'Author2', url: 'https://amazon.com', content: 'SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT', ups: '100', downs: '10', }, 3: { id: '3', title: 'Third book', author: 'Author3', url: null, content: null, ups: null, downs: null }, 4: { id: '4', title: 'Fourth book', author: 'Author4', url: 'https://www.amazon.com/', content: 'SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4', ups: '1000', downs: '0', }, 5: { id: '5', title: 'Fifth book', author: 'Author5', url: 'https://www.amazon.com/', content: 'SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT', ups: '50', downs: '0', }, } const relatedPosts = { 1: [posts['4']], 2: [posts['3'], posts['5']], 3: [posts['2'], posts['1']], 4: [posts['2'], posts['1']], 5: [], } if (!event.field && event.length){ console.log(`Got a BatchInvoke Request. The payload has ${event.length} items to resolve.`); return event.map(e => { // return an error for post 2 if (e.source.id === '2') { return { 'data': null, 'errorMessage': 'Error Happened', 'errorType': 'ERROR' } } return {data: relatedPosts[e.source.id]} }) } console.log('Got an Invoke Request.') let result switch (event.field) { case 'getPost': return posts[event.arguments.id] case 'allPosts': return Object.values(posts) case 'addPost': // return the arguments back return event.arguments case 'addPostErrorWithData': result = posts[event.arguments.id] // attached additional error information to the post result.errorMessage = 'Error with the mutation, data has changed' result.errorType = 'MUTATION_ERROR' return result case 'relatedPosts': return relatedPosts[event.source.id] default: throw new Error('Unknown field, unable to resolve ' + event.field) } }
Atualize o código do resolvedor relatedPosts:
import { util } from '@aws-appsync/utils'; export function request(ctx) { const {source, args} = ctx return { operation: ctx.info.fieldName === 'relatedPosts' ? 'BatchInvoke' : 'Invoke', payload: { field: ctx.info.fieldName, arguments: args, source }, }; } export function response(ctx) { const { error, result } = ctx; if (error) { util.appendError(error.message, error.type, result); } else if (result.errorMessage) { util.appendError(result.errorMessage, result.errorType, result.data) } else if (ctx.info.fieldName === 'relatedPosts') { return result.data } else { return result } }
O manipulador de respostas agora verifica erros retornados pela função do Lambda nas operações Invoke, verifica erros retornados para itens individuais das operações BatchInvoke e, por fim, verifica fieldName. Para relatedPosts, a função retorna result.data. Para todos os outros campos, a função retorna apenas result. Por exemplo, veja a consulta abaixo:
query AllPosts { allPosts { id title content url ups downs relatedPosts { id } author } }
Essa consulta retorna uma resposta do GraphQL semelhante à seguinte:
{ "data": { "allPosts": [ { "id": "1", "relatedPosts": [ { "id": "4" } ] }, { "id": "2", "relatedPosts": null }, { "id": "3", "relatedPosts": [ { "id": "2" }, { "id": "1" } ] }, { "id": "4", "relatedPosts": [ { "id": "2" }, { "id": "1" } ] }, { "id": "5", "relatedPosts": [] } ] }, "errors": [ { "path": [ "allPosts", 1, "relatedPosts" ], "data": null, "errorType": "ERROR", "errorInfo": null, "locations": [ { "line": 4, "column": 5, "sourceName": null } ], "message": "Error Happened" } ] }
Configuração do tamanho máximo do lote
Para configurar o tamanho máximo do lote em um resolvedor, use o seguinte comando na AWS Command Line Interface (AWS CLI):
$ aws appsync create-resolver --api-id <api-id> --type-name Query --field-name relatedPosts \ --code "<code-goes-here>" \ --runtime name=APPSYNC_JS,runtimeVersion=1.0.0 \ --data-source-name "<lambda-datasource>" \ --max-batch-size X
nota
Ao fornecer um modelo de mapeamento de solicitação, você deve usar a operação BatchInvoke para usar lotes.