Uso do Aurora PostgreSQL com a API de dados no AWS AppSync
O AWS AppSync fornece uma fonte de dados para executar declarações SQL em clusters do Amazon Aurora que foram habilitados com uma API de dados. É possível usar resolvedores do AWS AppSync para executar declarações SQL na API de dados com consultas, mutações e assinaturas do GraphQL.
nota
Este tutorial usa a Região US-EAST-1
.
Criar clusters
Antes de adicionar uma fonte de dados do Amazon RDS ao AWS AppSync, primeiro habilite uma API de dados em um cluster do Aurora Sem Servidor. Também é necessário configurar um segredo usando o AWS Secrets Manager. Para criar um cluster do Aurora Sem Servidor, é possível usar a AWS CLI:
aws rds create-db-cluster \ --db-cluster-identifier appsync-tutorial \ --engine aurora-postgresql --engine-version 13.11 \ --engine-mode serverless \ --master-username USERNAME \ --master-user-password COMPLEX_PASSWORD
Isso retornará um ARN para o cluster. É possível conferir o status do cluster com o comando:
aws rds describe-db-clusters \ --db-cluster-identifier appsync-tutorial \ --query "DBClusters[0].Status"
Crie um segredo por meio do console do AWS Secrets Manager ou da AWS CLI com um arquivo de entrada, como o seguinte, usando USERNAME
e COMPLEX_PASSWORD
da etapa anterior:
{ "username": "USERNAME", "password": "COMPLEX_PASSWORD" }
Transmita isso como um parâmetro para a CLI:
aws secretsmanager create-secret \ --name appsync-tutorial-rds-secret \ --secret-string file://creds.json
Isso retornará um ARN para o segredo. Anote o ARN do cluster do Aurora Sem Servidor e o segredo para uso posterior ao criar uma fonte de dados no console do AWS AppSync.
Habilitar a API de dados
Depois que o status do cluster mudar para available
, habilite a API de dados seguindo a documentação do Amazon RDS. A API de dados deve ser habilitada antes de ser adicionada como uma fonte de dados do AWS AppSync. Também é possível habilitar a API de dados usando a AWS CLI:
aws rds modify-db-cluster \ --db-cluster-identifier appsync-tutorial \ --enable-http-endpoint \ --apply-immediately
Criar o banco de dados e uma tabela
Depois de habilitar a API de dados, verifique se ela funciona usando o comando aws rds-data
execute-statement
na AWS CLI. Isso garantirá que o cluster do Aurora Sem Servidor esteja configurado corretamente antes de adicioná-lo à API do AWS AppSync. Primeiro, crie um banco de dados chamado TESTDB com o parâmetro --sql
:
aws rds-data execute-statement \ --resource-arn "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial" \ --secret-arn "arn:aws:secretsmanager:us-east-1:123456789012:secret:appsync-tutorial-rds-secret" \ --sql "create DATABASE \"testdb\""
Se isso for executado sem erros, inclua duas tabelas com o comando create table
:
aws rds-data execute-statement \ --resource-arn "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial" \ --secret-arn "arn:aws:secretsmanager:us-east-1:123456789012:secret:appsync-tutorial-rds-secret" \ --database "testdb" \ --sql 'create table public.todos (id serial constraint todos_pk primary key, description text not null, due date not null, "createdAt" timestamp default now());' aws rds-data execute-statement \ --resource-arn "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial" \ --secret-arn "arn:aws:secretsmanager:us-east-1:123456789012:secret:appsync-tutorial-rds-secret" \ --database "testdb" \ --sql 'create table public.tasks (id serial constraint tasks_pk primary key, description varchar, "todoId" integer not null constraint tasks_todos_id_fk references public.todos);'
Se a execução ocorrer sem problemas, será possível adicionar o cluster como uma fonte de dados à API.
Criar um esquema do GraphQL
Agora que a API de dados do Aurora Sem Servidor está sendo executada com tabelas configuradas, criaremos um esquema do GraphQL. É possível fazer isso manualmente, mas o AWS AppSync permite começar rapidamente importando a configuração da tabela de um banco de dados existente com o assistente de criação de API.
Para começar:
-
No console do AWS AppSync, selecione Criar API e, depois, Iniciar com um cluster do Amazon Aurora.
-
Especifique os detalhes da API, como Nome da API, e selecione o banco de dados para gerar a API.
-
Selecione o banco de dados. Se necessário, atualize a região e selecione o cluster do Aurora e o banco de dados TESTDB.
-
Selecione o segredo e escolha Importar.
-
Depois que as tabelas forem descobertas, atualize os nomes dos tipos. Altere
Todos
paraTodo
eTasks
paraTask
. -
Visualize o esquema gerado selecionando Visualizar esquema. O esquema terá a seguinte aparência:
type Todo { id: Int! description: String! due: AWSDate! createdAt: String } type Task { id: Int! todoId: Int! description: String }
-
Para o perfil, é possível fazer com que o AWS AppSync crie um perfil ou crie um com uma política semelhante a esta:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "rds-data:ExecuteStatement", ], "Resource": [ "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial", "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial:*" ] }, { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue" ], "Resource": [ "arn:aws:secretsmanager:us-east-1:123456789012:secret:your:secret:arn:appsync-tutorial-rds-secret", "arn:aws:secretsmanager:us-east-1:123456789012:secret:your:secret:arn:appsync-tutorial-rds-secret:*" ] } ] }
Observe que há duas declarações nesta política à qual você está concedendo acesso ao perfil. O primeiro recurso é o cluster do Aurora e o segundo é o ARN do AWS Secrets Manager.
Selecione Próximo, revise os detalhes da configuração e escolha Criar API. Agora você tem uma API totalmente operacional. É possível revisar os detalhes completos da API na página Esquema.
Resolvedores para RDS
O fluxo de criação da API criou automaticamente os resolvedores para interagir com nossos tipos. Se você consultar a página Esquema, encontrará os resolvedores necessários para:
-
Criar um
todo
por meio do campoMutation.createTodo
. -
Atualizar um
todo
por meio do campoMutation.updateTodo
. -
Excluir um
todo
por meio do campoMutation.deleteTodo
. -
Obter um único
todo
por meio do campoQuery.getTodo
. -
Listar todos os
todos
por meio do campoQuery.listTodos
.
Você encontrará campos e resolvedores semelhantes anexados para o tipo Task
. Vamos examinar com mais cuidado alguns dos resolvedores.
Mutation.createTodo
No editor de esquemas no console do AWS AppSync, à direita, selecione testdb
ao lado de createTodo(...): Todo
. O código do resolvedor usa a função insert
do módulo do rds
para criar dinamicamente uma declaração de inserção que adiciona dados à tabela todos
. Como estamos trabalhando com o Postgres, podemos aproveitar a declaração returning
para recuperar os dados inseridos.
Vamos atualizar o resolvedor para especificar corretamente o tipo DATE
do campo due
:
import { util } from '@aws-appsync/utils'; import { insert, createPgStatement, toJsonObject, typeHint } from '@aws-appsync/utils/rds'; export function request(ctx) { const { input } = ctx.args; // if a due date is provided, cast is as `DATE` if (input.due) { input.due = typeHint.DATE(input.due) } const insertStatement = insert({ table: 'todos', values: input, returning: '*', }); return createPgStatement(insertStatement) } export function response(ctx) { const { error, result } = ctx; if (error) { return util.appendError( error.message, error.type, result ) } return toJsonObject(result)[0][0] }
Salve o resolvedor. A dica de tipo marca a propriedade due
no objeto de entrada como um tipo DATE
. Isso permite que o mecanismo Postgres interprete adequadamente o valor. Depois, atualize o esquema para remover o id
da entrada CreateTodo
. Como nosso banco de dados Postgres pode exibir o ID gerado, podemos confiar nele para criar e exibir o resultado como uma única solicitação:
input CreateTodoInput { due: AWSDate! createdAt: String description: String! }
Faça a alteração e atualize o esquema. Acesse o editor de consultas para adicionar um item ao banco de dados:
mutation CreateTodo { createTodo(input: {description: "Hello World!", due: "2023-12-31"}) { id due description createdAt } }
Você obtém o resultado:
{ "data": { "createTodo": { "id": 1, "due": "2023-12-31", "description": "Hello World!", "createdAt": "2023-11-14 20:47:11.875428" } } }
Query.listTodos
No editor de esquemas no console, à direita, selecione testdb
ao lado de listTodos(id: ID!): Todo
. O manipulador de solicitações usa a função de utilitário select para criar uma solicitação dinamicamente em runtime.
export function request(ctx) { const { filter = {}, limit = 100, nextToken } = ctx.args; const offset = nextToken ? +util.base64Decode(nextToken) : 0; const statement = select({ table: 'todos', columns: '*', limit, offset, where: filter, }); return createPgStatement(statement) }
Queremos filtrar todos
com base na data due
. Vamos atualizar o resolvedor no qual converter valores due
em DATE
. Atualize a lista de importações e o manipulador de solicitações:
import { util } from '@aws-appsync/utils'; import * as rds from '@aws-appsync/utils/rds'; export function request(ctx) { const { filter: where = {}, limit = 100, nextToken } = ctx.args; const offset = nextToken ? +util.base64Decode(nextToken) : 0; // if `due` is used in a filter, CAST the values to DATE. if (where.due) { Object.entries(where.due).forEach(([k, v]) => { if (k === 'between') { where.due[k] = v.map((d) => rds.typeHint.DATE(d)); } else { where.due[k] = rds.typeHint.DATE(v); } }); } const statement = rds.select({ table: 'todos', columns: '*', limit, offset, where, }); return rds.createPgStatement(statement); } export function response(ctx) { const { args: { limit = 100, nextToken }, error, result, } = ctx; if (error) { return util.appendError(error.message, error.type, result); } const offset = nextToken ? +util.base64Decode(nextToken) : 0; const items = rds.toJsonObject(result)[0]; const endOfResults = items?.length < limit; const token = endOfResults ? null : util.base64Encode(`${offset + limit}`); return { items, nextToken: token }; }
Vamos testar a consulta. No editor de consultas:
query LIST { listTodos(limit: 10, filter: {due: {between: ["2021-01-01", "2025-01-02"]}}) { items { id due description } } }
Mutation.updateTodo
Também é possível update
um Todo
. No editor de consultas, vamos atualizar o primeiro item Todo
de id
1
.
mutation UPDATE { updateTodo(input: {id: 1, description: "edits"}) { description due id } }
Observe que é preciso especificar o id
do item que você está atualizando. Também é possível determinar uma condição para atualizar somente um item que atenda a condições específicas. Por exemplo, poderemos editar o item somente se a descrição começar com edits
:
mutation UPDATE { updateTodo(input: {id: 1, description: "edits: make a change"}, condition: {description: {beginsWith: "edits"}}) { description due id } }
Assim como processamos as operações create
e list
, podemos atualizar o resolvedor para converter o campo due
em DATE
. Salve essas alterações em updateTodo
:
import { util } from '@aws-appsync/utils'; import * as rds from '@aws-appsync/utils/rds'; export function request(ctx) { const { input: { id, ...values }, condition = {}, } = ctx.args; const where = { ...condition, id: { eq: id } }; // if `due` is used in a condition, CAST the values to DATE. if (condition.due) { Object.entries(condition.due).forEach(([k, v]) => { if (k === 'between') { condition.due[k] = v.map((d) => rds.typeHint.DATE(d)); } else { condition.due[k] = rds.typeHint.DATE(v); } }); } // if a due date is provided, cast is as `DATE` if (values.due) { values.due = rds.typeHint.DATE(values.due); } const updateStatement = rds.update({ table: 'todos', values, where, returning: '*', }); return rds.createPgStatement(updateStatement); } export function response(ctx) { const { error, result } = ctx; if (error) { return util.appendError(error.message, error.type, result); } return rds.toJsonObject(result)[0][0]; }
Agora tente uma atualização com uma condição:
mutation UPDATE { updateTodo( input: { id: 1, description: "edits: make a change", due: "2023-12-12"}, condition: { description: {beginsWith: "edits"}, due: {ge: "2023-11-08"}}) { description due id } }
Mutation.deleteTodo
É possível delete
um Todo
com a mutação deleteTodo
. Isso funciona como a mutação updateTodo
, e é necessário especificar o id
do item a ser excluído:
mutation DELETE { deleteTodo(input: {id: 1}) { description due id } }
Redigir consultas personalizadas
Usamos os utilitários do módulo do rds
para criar declarações SQL. Também podemos redigir a própria declaração estática personalizada para interagir com o banco de dados. Primeiro, atualize o esquema para remover o id
da entrada CreateTask
.
input CreateTaskInput { todoId: Int! description: String }
Depois, crie algumas tarefas. Uma tarefa tem uma relação de chave externa com Todo
:
mutation TASKS { a: createTask(input: {todoId: 2, description: "my first sub task"}) { id } b:createTask(input: {todoId: 2, description: "another sub task"}) { id } c: createTask(input: {todoId: 2, description: "a final sub task"}) { id } }
Crie um campo no tipo Query
chamado getTodoAndTasks
:
getTodoAndTasks(id: Int!): Todo
Adicione um campo tasks
ao tipo Todo
:
type Todo { due: AWSDate! id: Int! createdAt: String description: String! tasks:TaskConnection }
Salve o esquema. No editor de esquemas no console, à direita, selecione Anexar resolvedor para getTodosAndTasks(id:
Int!): Todo
. Selecione a fonte de dados do Amazon RDS. Atualize o resolvedor com o seguinte código:
import { sql, createPgStatement,toJsonObject } from '@aws-appsync/utils/rds'; export function request(ctx) { return createPgStatement( sql`SELECT * from todos where id = ${ctx.args.id}`, sql`SELECT * from tasks where "todoId" = ${ctx.args.id}`); } export function response(ctx) { const result = toJsonObject(ctx.result); const todo = result[0][0]; if (!todo) { return null; } todo.tasks = { items: result[1] }; return todo; }
Nesse código, usamos o modelo de tag sql
para redigir uma declaração SQL para a qual podemos transmitir com segurança um valor dinâmico em runtime. createPgStatement
pode receber até duas solicitações SQL por vez. Usamos isso para enviar uma consulta ao todo
e outra para a tasks
. É possível fazer isso com uma declaração JOIN
ou qualquer outro método. A ideia é poder redigir a própria declaração SQL para implementar a lógica de negócios. Para usar a consulta no editor de consultas, podemos tentar o seguinte:
query TodoAndTasks { getTodosAndTasks(id: 2) { id due description tasks { items { id description } } } }
Excluir o cluster
Importante
A exclusão de um cluster é permanente. Revise o projeto com cuidado antes de realizar essa ação.
Para excluir o cluster:
$ aws rds delete-db-cluster \ --db-cluster-identifier appsync-tutorial \ --skip-final-snapshot