Uso de solucionadores de AWS Lambda en AWS AppSync - AWS AppSync GraphQL

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Uso de solucionadores de AWS Lambda en AWS AppSync

Puede utilizar AWS Lambda con AWS AppSync para resolver cualquier campo de GraphQL. Por ejemplo, una consulta de GraphQL podría enviar una llamada a una instancia de Amazon Relational Database Service (Amazon RDS), y una mutación de GraphQL podría escribir en un flujo de Amazon Kinesis. En esta sección, veremos cómo puede escribir una función de lambda que ejecute la lógica de negocio en función de la invocación de una operación de campo de GraphQL.

Crear una función de Lambda

En el siguiente ejemplo se muestra una función de lambda escrita en Node.js (tiempo de ejecución: Node.js 18.x) que realiza distintas operaciones en publicaciones de blogs como parte de una aplicación de publicaciones en blogs. Tenga en cuenta que el código debe guardarse en un nombre de archivo con la extensión .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) } }

Esta función de Lambda recupera una publicación por identificador, añade una publicación, recupera una lista de publicaciones y recupera publicaciones relacionadas para una publicación determinada.

nota

La función de Lambda utiliza la instrucción switch en event.field para determinar qué campo se está resolviendo en ese momento.

Cree esta función de Lambda mediante la consola de administración de AWS.

Configure un origen de datos para Lambda

Una vez creada la función de Lambda, vaya a la API de GraphQL en la consola de AWS AppSync y elija la pestaña Orígenes de datos.

Elija Crear origen de datos, introduzca un Nombre de origen de datos fácil de recordar (por ejemplo, Lambda) y, a continuación, en Tipo de origen de datos, elija Función de AWS Lambda. En Región, elija la misma región que en su función. En ARN de función, elija el nombre de recurso de Amazon (ARN) para la función de Lambda.

Una vez seleccionada la función de Lambda, puede crear un nuevo rol de AWS Identity and Access Management (IAM) (al que AWS AppSync asignará los permisos adecuados) o bien elegir uno ya existente que tenga la política en línea siguiente:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": "arn:aws:lambda:REGION:ACCOUNTNUMBER:function/LAMBDA_FUNCTION" } ] }

También debe configurar una relación de confianza con AWS AppSync para el rol de IAM, de este modo:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "appsync.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }

Cree un esquema de GraphQL

Ahora que el origen de datos está conectado a la función de Lambda, cree un esquema de GraphQL.

En el editor de esquemas de la consola de AWS AppSync, asegúrese de que su esquema coincida con el siguiente:

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] }

Configure solucionadores

Ahora que ha registrado un origen de datos de Lambda y un esquema de GraphQL válido, puede conectar sus campos de GraphQL al origen de datos de Lambda utilizando solucionadores.

Creará un solucionador que utilice el tiempo de ejecución JavaScript (APPSYNC_JS) de AWS AppSync y que interactúe con las funciones de Lambda. Para obtener más información sobre cómo escribir solucionadores de AWS AppSync y funciones con JavaScript, consulte JavaScript runtime features for resolvers and functions.

Para obtener más información sobre las plantillas de mapeo de Lambda, consulte JavaScript resolver function reference for Lambda.

En este paso, debe asociar un solucionador a la función de Lambda para los siguientes campos: getPost(id:ID!): Post, allPosts: [Post], addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! y Post.relatedPosts: [Post]. En el editor Esquema de la consola de AWS AppSync, en el panel Solucionadores, elija Asociar junto al campo getPost(id:ID!): Post. Elija el origen de datos de Lambda. A continuación, introduzca el siguiente 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; }

Este código de solucionador pasa el nombre del campo, la lista de argumentos y el contexto del objeto de origen a la función de Lambda cuando la invoca. Seleccione Guardar.

Acaba de asociar su primer solucionador. Repita esta operación para el resto de los campos.

Pruebe la API de GraphQL

Ahora que la función de Lambda está conectada a los solucionadores de GraphQL, puede ejecutar algunas mutaciones y consultas con la consola o una aplicación cliente.

A la izquierda de la consola de AWS AppSync, elija la pestaña Consultas y pegue el código siguiente:

Mutación 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 } } }

Devolución de errores

Cualquier resolución de campo dada puede producir un error. Con AWS AppSync, puede generar errores de los orígenes siguientes:

  • Controlador de respuestas del solucionador

  • Función de Lambda

Desde el controlador de respuestas del solucionador

Para generar errores intencionados, puede utilizar el método de la utilidad util.error. Toma como argumento un errorMessage, un errorType y un valor opcional de data. El argumento data es útil para devolver datos adicionales al cliente cuando se produce un error. El objeto data se añade a errors en la respuesta final de GraphQL.

En el ejemplo siguiente se muestra cómo utilizarlo en el controlador de respuestas del solucionador de Post.relatedPosts: [Post].

// the Post.relatedPosts response handler export function response(ctx) { util.error("Failed to fetch relatedPosts", "LambdaFailure", ctx.result) return ctx.result; }

Así se obtiene una respuesta de GraphQL similar a la siguiente:

{ "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" } ] } ] }

donde allPosts[0].relatedPosts es null debido al error y errorMessage, errorType y data se incluyen en el objeto data.errors[0].

Desde la función de Lambda

AWS AppSync también entiende los errores que produce la función de Lambda. El modelo de programación de Lambda permite generar errores gestionados. Si la función de lambda produce un error, AWS AppSync no puede resolver el campo actual. La respuesta solo incluirá el mensaje de error que devuelva Lambda. Actualmente no es posible devolver datos adicionales al cliente generando un error desde la función de Lambda.

nota

Si su función de Lambda genera un error no gestionado, AWS AppSync utiliza el mensaje de error que Lambda estableció.

La siguiente función de Lambda genera un error:

export const handler = async (event) => { console.log('Received event {}', JSON.stringify(event, 3)) throw new Error('I always fail.') }

El error se recibe en el controlador de respuestas. Puede devolverlo en la respuesta de GraphQL añadiendo el error a la respuesta con util.appendError. Para ello, cambie el controlador de respuestas de la función de AWS AppSync por el siguiente:

// the lambdaInvoke response handler export function response(ctx) { const { error, result } = ctx; if (error) { util.appendError(error.message, error.type, result); } return result; }

Así se obtiene una respuesta de GraphQL similar a la siguiente:

{ "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 avanzado: agrupación en lotes

La función de Lambda de este ejemplo tiene un campo relatedPosts que devuelve una lista de publicaciones relacionadas para una publicación determinada. En las consultas del ejemplo, la invocación al campo allPosts desde la función de Lambda devuelve cinco publicaciones. Dado que hemos especificado que también queremos resolver relatedPosts para cada publicación obtenida, la operación del campo relatedPosts se invoca cinco veces.

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 } } }

Aunque no parezca mucho en este ejemplo concreto, esta sobrecarga compuesta puede perjudicar rápidamente a la aplicación.

Si quisiéramos obtener relatedPosts otra vez para todos los elementos de Posts en la misma consulta, el número de invocaciones aumentaría exponencialmente.

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 } } } }

En esta consulta relativamente sencilla, AWS AppSync invocaría la función de Lambda 1 + 5 + 25 = 31 veces.

Se trata de una situación bastante habitual que a menudo se denomina "problema N+1", (en nuestro caso, N = 5) y puede causar un aumento de la latencia y del costo de la aplicación.

Una forma de solucionarlo es agrupar por lotes las solicitudes de solucionador de campo similares. En este ejemplo, en lugar de hacer que la función de Lambda obtenga una lista de publicaciones relacionadas con una publicación individual determinada, hacemos que obtenga una lista de publicaciones relacionadas con un lote de publicaciones dado.

Para demostrarlo, actualicemos el solucionador para que relatedPosts gestione la agrupación en 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; }

El código ahora cambia la operación de Invoke a BatchInvoke cuando el fieldName que se está resolviendo es relatedPosts. Ahora, habilite la agrupación en lotes en la función en la sección Configuración de la agrupación en lotes. Establezca el tamaño máximo de la agrupación en lotes en 5. Seleccione Guardar.

Con este cambio, al resolver relatedPosts, la función de Lambda recibe lo siguiente como entrada:

[ { "field": "relatedPosts", "source": { "id": 1 } }, { "field": "relatedPosts", "source": { "id": 2 } }, ... ]

Cuando se especifica BatchInvoke en la solicitud, la función de Lambda recibe una lista de solicitudes y devuelve una lista de resultados.

En concreto, la lista de resultados debe coincidir en tamaño y orden con las entradas de la carga de la solicitud, por lo que AWS AppSync puede hacer coincidir los resultados como corresponda.

En este ejemplo de agrupación en lotes, la función de Lambda devuelve un lote de resultados de este modo:

[ [{"id":"2","title":"Second book"}, {"id":"3","title":"Third book"}], // relatedPosts for id=1 [{"id":"3","title":"Third book"}] // relatedPosts for id=2 ]

Puede actualizar el código de Lambda para gestionar la agrupación en 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) } }

Devolución de errores individuales

Los ejemplos anteriores muestran que es posible devolver un único error desde la función de Lambda o generar un error desde su controlador de respuestas. En las invocaciones en lotes, la generación de un error desde la función de Lambda marca como fallido todo el lote. Esto puede ser adecuado en situaciones concretas donde se haya producido un error irrecuperable, como, por ejemplo, un error de conexión a un almacén de datos. Sin embargo, en los casos en los que algunos elementos del lote se ejecutan correctamente y otros fallan, es posible devolver tanto los errores como los datos válidos. Puesto que AWS AppSync requiere que en la respuesta por lotes se enumeren los elementos que coinciden con el tamaño original del lote, debe definir una estructura de datos que pueda diferenciar los datos válidos de un error.

Por ejemplo, si se espera que la función de Lambda devuelva un lote de publicaciones relacionadas, podría optar por devolver una lista de objetos Response en la que cada objeto tenga campos opcionales data, errorMessage y errorType. Si el campo errorMessage está presente, significa que se ha producido un error.

El código siguiente muestra cómo podría actualizar la función de 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) } }

Actualice el código del solucionador 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 } }

El controlador de respuestas ahora comprueba los errores devueltos por la función de Lambda en las operaciones Invoke, comprueba los errores devueltos para elementos individuales de las operaciones BatchInvoke y, finalmente, comprueba el fieldName. Para relatedPosts, la función devuelve result.data. Para el resto de los campos, la función simplemente devuelve result. Por ejemplo, veamos la siguiente consulta:

query AllPosts { allPosts { id title content url ups downs relatedPosts { id } author } }

Esta consulta devuelve una respuesta de GraphQL similar a la siguiente:

{ "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" } ] }

Configuración del tamaño máximo de agrupación en lotes

Para configurar el tamaño máximo de agrupación en lotes en un solucionador, utilice el siguiente comando en la 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

Al proporcionar una plantilla de mapeo de solicitudes, debe usar la operación BatchInvoke para usar la agrupación en lotes.