Utilizzo dei AWS Lambda resolver in AWS AppSync - AWS AppSync GraphQL

Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.

Utilizzo dei AWS Lambda resolver in AWS AppSync

Puoi usare AWS Lambda with AWS AppSync per risolvere qualsiasi campo GraphQL. Ad esempio, una query GraphQL potrebbe inviare una chiamata a un'istanza Amazon Relational Database Service (AmazonRDS) e una mutazione GraphQL potrebbe scrivere su un flusso Amazon Kinesis. In questa sezione, ti mostreremo come scrivere una funzione Lambda che esegue la logica aziendale basata sull'invocazione di un'operazione sul campo GraphQL.

Creazione di una funzione Lambda

L'esempio seguente mostra una funzione Lambda scritta in Node.js (runtime: Node.js 18.x) che esegue diverse operazioni sui post del blog come parte di un'applicazione per post di blog. Nota che il codice deve essere salvato in un nome di file con estensione.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) } }

Questa funzione Lambda recupera un post per ID, aggiunge un post, recupera un elenco di post e recupera i post correlati per un determinato post.

Nota

La funzione Lambda utilizza l'switchistruzione on event.field per determinare quale campo è attualmente in fase di risoluzione.

Crea questa funzione Lambda utilizzando la console di AWS gestione.

Configurare un'origine dati per Lambda

Dopo aver creato la funzione Lambda, accedi a GraphQL API nella AWS AppSync console, quindi scegli la scheda Data Sources.

Scegli Crea origine dati, inserisci un nome di origine dati descrittivo (ad esempio,Lambda), quindi per Tipo di origine dati, scegli AWS Lambda funzione. Per Regione, scegli la stessa regione della tua funzione. Per Funzione ARN, scegli Amazon Resource Name (ARN) della tua funzione Lambda.

Dopo aver scelto la funzione Lambda, puoi creare un nuovo ruolo AWS Identity and Access Management (IAM) (per il quale AWS AppSync assegna le autorizzazioni appropriate) o scegliere un ruolo esistente con la seguente politica in linea:

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

È inoltre necessario impostare una relazione di fiducia con AWS AppSync per il IAM ruolo come segue:

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

Creare uno schema GraphQL

Ora che l'origine dati è connessa alla funzione Lambda, crea uno schema GraphQL.

Dall'editor di schemi nella AWS AppSync console, assicurati che lo schema corrisponda allo schema seguente:

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

Configura i resolver

Ora che hai registrato un'origine dati Lambda e uno schema GraphQL valido, puoi connettere i campi GraphQL all'origine dati Lambda utilizzando i resolver.

Creerai un resolver che utilizza il runtime AWS AppSync JavaScript (APPSYNC_JS) e interagirà con le tue funzioni Lambda. Per ulteriori informazioni sulla scrittura di AWS AppSync resolver e funzioni con JavaScript, consulta le funzionalità di JavaScript runtime per resolver e funzioni.

Per ulteriori informazioni sui modelli di mappatura Lambda, consulta il riferimento alla JavaScript funzione resolver per Lambda.

In questo passaggio, si collega un resolver alla funzione Lambda per i seguenti campi:getPost(id:ID!): Post,, allPosts: [Post] e. addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! Post.relatedPosts: [Post] Dall'editor di schema della AWS AppSync console, nel riquadro Resolver, scegli Allega accanto al campo. getPost(id:ID!): Post Scegli la tua fonte di dati Lambda. Quindi, fornisci il seguente codice:

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

Questo codice resolver passa il nome del campo, l'elenco di argomenti e il contesto dell'oggetto sorgente alla funzione Lambda quando viene richiamata. Seleziona Salva.

Il primo resolver è stato ora collegato con successo. Ripetere questa operazione per i campi rimanenti.

Testa il tuo GraphQL API

Ora che la funzione Lambda è connessa ai resolver GraphQL, puoi eseguire alcune mutazioni e query usando la console o un'applicazione client.

Sul lato sinistro della AWS AppSync console, scegli Query, quindi incolla il codice seguente:

addPost Mutazione

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

getPost Domanda

query GetPost { getPost(id: "2") { id author title content url ups downs } }

allPosts Domanda

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

Errori di restituzione

Qualsiasi risoluzione di campo può causare un errore. Con AWS AppSync, puoi generare errori dalle seguenti fonti:

  • Gestore di risposte Resolver

  • Funzione Lambda

Dal gestore di risposte del resolver

Per generare errori intenzionali, è possibile utilizzare il metodo di utilità. util.error Richiede un argomento an errorMessageerrorType, un e un data valore opzionale. Il valore data è utile per restituire dati aggiuntivi al client quando è stato generato un errore. L'oggetto data verrà aggiunto a errors nella risposta finale di GraphQL.

L'esempio seguente mostra come usarlo nel gestore di risposte del Post.relatedPosts: [Post] resolver.

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

Ciò restituirà una risposta di GraphQL simile alla seguente:

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

Dove allPosts[0].relatedPosts è null a causa dell'errore e errorMessage, errorType e data sono presenti nell'oggetto data.errors[0].

Dalla funzione Lambda

AWS AppSync comprende anche gli errori generati dalla funzione Lambda. Il modello di programmazione Lambda consente di generare errori gestiti. Se la funzione Lambda genera un errore, AWS AppSync non riesce a risolvere il campo corrente. Nella risposta viene impostato solo il messaggio di errore restituito da Lambda. Attualmente, non è possibile restituire dati estranei al client generando un errore dalla funzione Lambda.

Nota

Se la funzione Lambda genera un errore non gestito, AWS AppSync utilizza il messaggio di errore impostato da Lambda.

La funzione Lambda seguente genera un errore:

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

L'errore viene ricevuto nel gestore delle risposte. Puoi rispedirlo nella risposta GraphQL aggiungendo l'errore alla risposta con. util.appendError Per fare ciò, modifica il gestore AWS AppSync della risposta alla funzione in questo modo:

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

Ciò restituirà una risposta di GraphQL simile alla seguente:

{ "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 d'uso avanzato: Batching

La funzione Lambda in questo esempio ha un relatedPosts campo che restituisce un elenco di post correlati per un determinato post. Nelle query di esempio, l'invocazione del allPosts campo dalla funzione Lambda restituisce cinque post. Poiché abbiamo specificato che vogliamo risolvere anche relatedPosts per ogni post restituito, l'operazione relatedPosts sul campo viene richiamata cinque volte.

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

Anche se questo potrebbe non sembrare importante in questo esempio specifico, questo eccesso di recupero aggravato può compromettere rapidamente l'applicazione.

Se, ad esempio, dovessimo recuperare di nuovo relatedPosts sui Posts correlati restituiti nella stessa query, il numero di chiamate aumenterebbe notevolmente.

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

In questa query relativamente semplice, AWS AppSync richiamerebbe la funzione Lambda 1 + 5 + 25 = 31 volte.

Si tratta di una sfida piuttosto comune a cui si fa spesso riferimento come problema N+1, (nel nostro caso, N = 5) che può determinare un aumento della latenza e dei costi dell'applicazione.

Un approccio alla soluzione di questo problema consiste nel raggruppare in batch richieste del resolver di campo simili. In questo esempio, invece di fare in modo che la funzione Lambda risolva un elenco di post correlati per un singolo post, potrebbe invece risolvere un elenco di post correlati per un determinato batch di post.

Per dimostrarlo, aggiorniamo il resolver per relatedPosts gestire il batching.

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

Il codice ora modifica l'operazione da Invoke a BatchInvoke quando è in fieldName corso la risoluzione. relatedPosts Ora, abilita il batching sulla funzione nella sezione Configura batch. Imposta la dimensione massima di batch impostata su. 5 Seleziona Salva.

Con questa modifica, durante la risoluzionerelatedPosts, la funzione Lambda riceve quanto segue come input:

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

Quando BatchInvoke è specificata nella richiesta, la funzione Lambda riceve un elenco di richieste e restituisce un elenco di risultati.

In particolare, l'elenco dei risultati deve corrispondere alla dimensione e all'ordine delle voci del payload della richiesta in modo che AWS AppSync possa corrispondere ai risultati di conseguenza.

In questo esempio di batch, la funzione Lambda restituisce un batch di risultati come segue:

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

Puoi aggiornare il codice Lambda per gestire il batch per: 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) } }

Restituzione di errori individuali

Gli esempi precedenti mostrano che è possibile restituire un singolo errore dalla funzione Lambda o generare un errore dal gestore delle risposte. Per le chiamate in batch, la generazione di un errore dalla funzione Lambda contrassegna un intero batch come fallito. Questo potrebbe essere accettabile per scenari specifici in cui si verifica un errore irreversibile, ad esempio una connessione non riuscita a un data store. Tuttavia, nei casi in cui alcuni elementi del batch abbiano esito positivo e altri falliscano, è possibile restituire sia errori che dati validi. Poiché AWS AppSync richiede la risposta in batch agli elementi dell'elenco che corrispondono alla dimensione originale del batch, è necessario definire una struttura di dati in grado di differenziare i dati validi da un errore.

Ad esempio, se si prevede che la funzione Lambda restituisca un batch di post correlati, è possibile scegliere di restituire un elenco di Response oggetti in cui ogni oggetto contiene dati e errorTypecampi opzionali. errorMessage Se il errorMessagecampo è presente, significa che si è verificato un errore.

Il codice seguente mostra come aggiornare la funzione 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) } }

Aggiorna il codice del relatedPosts resolver:

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

Il gestore delle risposte ora controlla gli errori restituiti dalla funzione Lambda Invoke sulle operazioni, controlla gli errori restituiti per i singoli elementi BatchInvoke per le operazioni e infine controlla il. fieldName PerchérelatedPosts, la funzione restituisce. result.data Per tutti gli altri campi, la funzione si limita a restituireresult. Ad esempio, vedi la query seguente:

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

Questa query restituisce una risposta GraphQL simile alla seguente:

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

Configurazione della dimensione massima di batch

Per configurare la dimensione massima di batch su un resolver, utilizzate il seguente comando in (): 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

Quando si fornisce un modello di mappatura delle richieste, è necessario utilizzare l'BatchInvokeoperazione per utilizzare il batch.